Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion changelogs/2026-01-29_17-52-23.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
- Changed the `create_env_file` function to only create directories without changing their permissions, avoiding potential permission errors.

### Fixed
- No explicit bug fixes mentioned, but several changes improve the handling of environment variables and secrets, potentially fixing related issues.
- No explicit bug fixes mentioned, but several changes improve the handling of environment variables and secrets, potentially fixing related issues.
15 changes: 10 additions & 5 deletions src/env_manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import json
import os
import re
Expand All @@ -6,6 +7,7 @@
import yaml

from src import config
from src.connection import run_command


def parse_all_in_one_secret(secret_content: str, format_hint: str = "auto") -> Dict[str, str]:
Expand Down Expand Up @@ -368,16 +370,19 @@ def detect_environment_secrets() -> Dict[str, Dict[str, str]]:


def create_env_file(conn, file_path: str, env_vars: Dict[str, str]) -> None:
"""Create .env file with secure permissions (0o600)"""
"""Create .env file with secure permissions (0o600) via run_command (supports sudo)"""
if not env_vars:
return
dir_path = os.path.dirname(file_path)
if dir_path and dir_path != file_path:
# Only mkdir, skip chmod on directory to avoid permission errors if it exists/owned by others
conn.run(f"mkdir -p {dir_path}")
# Only mkdir, skip chmod on directory to avoid permission errors
run_command(conn, f"mkdir -p {dir_path}")

env_content = "\n".join([f"{k}={v}" for k, v in env_vars.items()])
conn.run(f"cat > \"{file_path}\" << 'EOF'\n{env_content}\nEOF")
conn.run(f'chmod 644 "{file_path}"')
# Use base64 to avoid shell character/newline mangling issues
encoded = base64.b64encode(env_content.encode("utf-8")).decode("utf-8")
run_command(conn, f"echo '{encoded}' | base64 -d | tee \"{file_path}\" > /dev/null")
run_command(conn, f'chmod 600 "{file_path}"')


def generate_env_files(conn) -> None:
Expand Down
21 changes: 13 additions & 8 deletions tests/unit/test_env_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,16 @@ def test_root_mega_file_creation(mock_conn, monkeypatch):

def test_heredoc_escaping(mock_conn):
env_vars = {"KEY": "val$with$dollars"}
create_env_file(mock_conn, ".env", env_vars)
found_secure_cat = False
for call in mock_conn.run.call_args_list:
cmd = call[0][0]
if "cat >" in cmd and "<< 'EOF'" in cmd:
found_secure_cat = True
assert "KEY=val$with$dollars" in cmd
assert found_secure_cat
# We need to mock run_command because create_env_file now uses it
with patch("src.env_manager.run_command") as mock_run_cmd:
create_env_file(mock_conn, ".env", env_vars)

found_base64_tee = False
for call in mock_run_cmd.call_args_list:
cmd = call[0][1]
if "base64 -d" in cmd and "tee" in cmd:
found_base64_tee = True
# The content should be base64 encoded
# KEY=val$with$dollars -> S0VZPXZhbCR3aXRoJGRvbGxhcnM=
assert "S0VZPXZhbCR3aXRoJGRvbGxhcnM=" in cmd
assert found_base64_tee