Skip to content

fix: set file permissions on AWS credentials, remove sensitive data from SSH logs#1907

Open
jlima8900 wants to merge 5 commits intoKeeper-Security:masterfrom
jlima8900:fix/security-audit-findings-pr1873
Open

fix: set file permissions on AWS credentials, remove sensitive data from SSH logs#1907
jlima8900 wants to merge 5 commits intoKeeper-Security:masterfrom
jlima8900:fix/security-audit-findings-pr1873

Conversation

@jlima8900
Copy link
Copy Markdown

Summary

Two validated findings from a security audit of the Commander codebase (445 Python files, 10 categories, 3-pass validation).

KC-2026-001: AWS credentials file permissions

The awskey rotation command writes credentials to ~/.aws/credentials without restricting file permissions, leaving them world-readable by default.

KC-2026-002: SSH connection logging

SSH connection handler logs contain sensitive session data that should be redacted.

Test plan

  • Verified file permissions set to 600 on AWS credentials
  • Verified sensitive data removed from SSH logs

- AWS credentials file: use os.open() with 0o600 at creation time
  to eliminate TOCTOU race window (was open()+chmod())
- SSH plugin: remove sensitive data from debug log messages
Address PR Keeper-Security#1873 review from @aaunario-keeper: use restrictive umask
around shutil.copy2 for the backup file so it's never world-readable,
even briefly. Removes the post-hoc os.chmod call.
Address review feedback:
- Use os.open() with O_CREAT | 0o600 to set permissions at file creation
- Removes race window where file existed with default umask permissions
- Remove now-redundant os.chmod() call
Address review feedback:
- Narrow json.loads catches to JSONDecodeError only (drop unreachable ValueError/TypeError/KeyError)
- Add e.msg, e.lineno, e.colno to log messages for better debuggability
- Split import_schedule_field handler into JSONDecodeError vs (ValueError, KeyError)
…existing files

- Replace open(fd, 'w') with os.fdopen(fd, 'w') to prevent double-close
  on write failure (with-block closes fd, then bare except also called
  os.close(fd))
- Add os.fchmod(fd, 0o600) so existing files also get restrictive
  permissions (os.open mode only applies on creation)
- Remove bare except clause
- Add 19 tests covering file permissions, log sanitization, and
  exception narrowing
obj = cls()
try: data = json.loads(data) if isinstance(data, str) else data
except: logging.error(f"Pam Scripts failed to load from: {str(data)[:80]}...")
except json.JSONDecodeError as e: logging.error(f"Pam Scripts: invalid JSON at line {e.lineno}, col {e.colno} — {e.msg}. Input (truncated): {str(data)[:80]}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -463,7 +463,7 @@ def load(cls, data: Union[str, dict]) -> PamRotationScheduleObject:

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -478,7 +478,11 @@ def load(cls, data: Union[str, dict]) -> PamRotationScheduleObject:

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -517,7 +521,7 @@ def load(cls, data: Optional[Union[str, dict]], rotation_params: Optional[PamRot

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
@@ -590,7 +594,7 @@ def __init__(self):
def load(cls, data: Union[str, dict]):

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -1665,7 +1669,7 @@ def __init__(self):
def load(cls, data: Union[str, dict], rotation_params: Optional[PamRotationParams] = None):
obj = cls()
try: data = json.loads(data) if isinstance(data, str) else data

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -1826,7 +1830,7 @@ def __init__(self, disableCopy: Optional[bool] = None, disablePaste: Optional[bo
def load(cls, data: Union[str, dict]):
obj = cls()
try: data = json.loads(data) if isinstance(data, str) else data

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -1857,7 +1861,7 @@ def __init__(self, enableSftp: Optional[bool] = None, sftpRootDirectory: Optiona
def load(cls, data: Union[str, dict]):
obj = cls()
try: data = json.loads(data) if isinstance(data, str) else data

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
@@ -1896,7 +1900,7 @@ def __init__(
def load(cls, data: Union[str, dict]):
obj = cls()
try: data = json.loads(data) if isinstance(data, str) else data

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
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.

1 participant