Skip to content
Open
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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-02-11 - Secure File Creation with umask

Check failure on line 1 in .jules/sentinel.md

View workflow job for this annotation

GitHub Actions / Lint Documentation

First line in a file should be a top-level heading

.jules/sentinel.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## 2025-02-11 - Secure File Cr..."] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md

Check failure on line 1 in .jules/sentinel.md

View workflow job for this annotation

GitHub Actions / Lint Documentation

Headings should be surrounded by blank lines

.jules/sentinel.md:1 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## 2025-02-11 - Secure File Creation with umask"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md
**Vulnerability:** SSH private keys were created with default umask (often 022/002), making them world-readable for a brief window before `chmod` (TOCTOU race condition).

Check failure on line 2 in .jules/sentinel.md

View workflow job for this annotation

GitHub Actions / Lint Documentation

Line length

.jules/sentinel.md:2:81 MD013/line-length Line length [Expected: 80; Actual: 170] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md
**Learning:** Redirection `>` in shell scripts respects current umask, creating files with potentially insecure permissions by default. `chmod` after creation is insufficient for high-security files.

Check failure on line 3 in .jules/sentinel.md

View workflow job for this annotation

GitHub Actions / Lint Documentation

Line length

.jules/sentinel.md:3:81 MD013/line-length Line length [Expected: 80; Actual: 199] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md
**Prevention:** Wrap sensitive file creation commands in a subshell with `umask 077` (e.g., `(umask 077; command > file)`). This ensures atomic secure creation.

Check failure on line 4 in .jules/sentinel.md

View workflow job for this annotation

GitHub Actions / Lint Documentation

Line length

.jules/sentinel.md:4:81 MD013/line-length Line length [Expected: 80; Actual: 160] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md
Comment on lines +1 to +4
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Fix markdown lint violations flagged by CI.

The linter reports several issues: missing top-level heading (MD041), no blank line after the ## heading (MD022), and lines 2–4 exceed the 80-character limit (MD013). Also, the date reads 2025-02-11 β€” should this be 2026-02-11?

Suggested structure
-## 2025-02-11 - Secure File Creation with umask
-**Vulnerability:** SSH private keys were created with default umask (often 022/002), making them world-readable for a brief window before `chmod` (TOCTOU race condition).
-**Learning:** Redirection `>` in shell scripts respects current umask, creating files with potentially insecure permissions by default. `chmod` after creation is insufficient for high-security files.
-**Prevention:** Wrap sensitive file creation commands in a subshell with `umask 077` (e.g., `(umask 077; command > file)`). This ensures atomic secure creation.
+# Sentinel Security Notes
+
+## 2026-02-11 - Secure File Creation with umask
+
+**Vulnerability:** SSH private keys were created with default umask
+(often 022/002), making them world-readable for a brief window
+before `chmod` (TOCTOU race condition).
+
+**Learning:** Redirection `>` in shell scripts respects current
+umask, creating files with potentially insecure permissions by
+default. `chmod` after creation is insufficient for high-security
+files.
+
+**Prevention:** Wrap sensitive file creation commands in a subshell
+with `umask 077` (e.g., `(umask 077; command > file)`).
+This ensures atomic secure creation.
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 2025-02-11 - Secure File Creation with umask
**Vulnerability:** SSH private keys were created with default umask (often 022/002), making them world-readable for a brief window before `chmod` (TOCTOU race condition).
**Learning:** Redirection `>` in shell scripts respects current umask, creating files with potentially insecure permissions by default. `chmod` after creation is insufficient for high-security files.
**Prevention:** Wrap sensitive file creation commands in a subshell with `umask 077` (e.g., `(umask 077; command > file)`). This ensures atomic secure creation.
# Sentinel Security Notes
## 2026-02-11 - Secure File Creation with umask
**Vulnerability:** SSH private keys were created with default umask
(often 022/002), making them world-readable for a brief window
before `chmod` (TOCTOU race condition).
**Learning:** Redirection `>` in shell scripts respects current
umask, creating files with potentially insecure permissions by
default. `chmod` after creation is insufficient for high-security
files.
**Prevention:** Wrap sensitive file creation commands in a subshell
with `umask 077` (e.g., `(umask 077; command > file)`).
This ensures atomic secure creation.
🧰 Tools
πŸͺ› GitHub Check: Lint Documentation

[failure] 4-4: Line length
.jules/sentinel.md:4:81 MD013/line-length Line length [Expected: 80; Actual: 160] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md


[failure] 3-3: Line length
.jules/sentinel.md:3:81 MD013/line-length Line length [Expected: 80; Actual: 199] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md


[failure] 2-2: Line length
.jules/sentinel.md:2:81 MD013/line-length Line length [Expected: 80; Actual: 170] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md


[failure] 1-1: First line in a file should be a top-level heading
.jules/sentinel.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## 2025-02-11 - Secure File Cr..."] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md


[failure] 1-1: Headings should be surrounded by blank lines
.jules/sentinel.md:1 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## 2025-02-11 - Secure File Creation with umask"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md

πŸ€– Prompt for AI Agents
In @.jules/sentinel.md around lines 1 - 4, Add a top-level H1 (e.g., a brief
title) above the existing "## 2025-02-11 - Secure File Creation with umask"
heading, insert a blank line immediately after that existing `##` heading to
satisfy MD022, update the date in the heading from 2025-02-11 to 2026-02-11, and
reflow or break the long sentences in the three lines describing
Vulnerability/Learning/Prevention so each line is ≀80 characters to satisfy
MD013 (ensure punctuation and sentence meaning remain intact while wrapping).

78 changes: 78 additions & 0 deletions tests/test_ssh_creation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash
set -e

# Setup mock environment
TEST_HOME=$(mktemp -d)
export HOME="$TEST_HOME"
export XDG_CONFIG_HOME="$TEST_HOME/.config"
mkdir -p "$XDG_CONFIG_HOME/dotfiles"

# Create a wrapper script for op because export -f might not work if script calls op directly via PATH
mkdir -p "$TEST_HOME/bin"
cat <<'EOF' > "$TEST_HOME/bin/op"
#!/bin/bash
mock_op() {
if [[ "$1" == "account" && "$2" == "list" ]]; then
return 0 # Simulate signed in
elif [[ "$1" == "item" && "$2" == "get" ]]; then
return 0 # Simulate key exists
elif [[ "$1" == "read" ]]; then
if [[ "$2" == *"private_key"* ]]; then
echo "mock-private-key-content"
else
echo "mock-public-key-content"
fi
else
echo "mock-op-called-with: $@" >&2
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Unquoted $@ in mock script.

$@ should be "$@" to preserve argument boundaries containing spaces (e.g., a key name like "SSH Key").

-        echo "mock-op-called-with: $@" >&2
+        echo "mock-op-called-with: $*" >&2
πŸ€– Prompt for AI Agents
In `@tests/test_ssh_creation.sh` at line 26, The mock script prints its arguments
with echo "mock-op-called-with: $@" which loses argument boundaries for values
containing spaces; update the echo invocation in tests/test_ssh_creation.sh to
use quoted expansion ("$@") so arguments like "SSH Key" remain a single
parameter when printed by the mock.

return 0
fi
}
mock_op "$@"
EOF
chmod +x "$TEST_HOME/bin/op"
export PATH="$TEST_HOME/bin:$PATH"

# Setup yq mock if needed (script uses yq to read config)
# But we pass --vault and --name so it might skip config reading or use defaults.
# If yq is missing, script might fail or fallback.
# load_config checks command -v yq.
# Let's verify if yq is installed in the environment.
if ! command -v yq &>/dev/null; then
# Mock yq
cat <<'EOF' > "$TEST_HOME/bin/yq"
#!/bin/bash
echo "null"
EOF
chmod +x "$TEST_HOME/bin/yq"
fi

# Run the restore command
# We use --vault and --name to bypass interactive prompt if needed.
# Since local key doesn't exist, cmd_restore should run without prompting for overwrite.

echo "Running setup-ssh-keys.sh restore..."
./tools/setup-ssh-keys.sh restore --vault test --name test-key

# Check permissions
KEY_FILE="$TEST_HOME/.ssh/id_ed25519"
SSH_DIR="$TEST_HOME/.ssh"

if [[ ! -f "$KEY_FILE" ]]; then
echo "FAIL: Key file not created"
exit 1
fi

PERMS=$(stat -c "%a" "$KEY_FILE")
if [[ "$PERMS" != "600" ]]; then
echo "FAIL: Private key permissions are $PERMS (expected 600)"
exit 1
fi

DIR_PERMS=$(stat -c "%a" "$SSH_DIR")
if [[ "$DIR_PERMS" != "700" ]]; then
echo "FAIL: SSH directory permissions are $DIR_PERMS (expected 700)"
exit 1
Comment on lines +65 to +74
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

stat -c is Linux-only β€” this will fail on macOS.

The script under test references brew, strongly suggesting macOS as the primary target. On macOS, stat -c "%a" is unsupported; use stat -f "%Lp" instead.

Portable helper
+get_perms() {
+    if stat -c "%a" /dev/null &>/dev/null 2>&1; then
+        stat -c "%a" "$1"
+    else
+        stat -f "%Lp" "$1"
+    fi
+}
+
-PERMS=$(stat -c "%a" "$KEY_FILE")
+PERMS=$(get_perms "$KEY_FILE")
 if [[ "$PERMS" != "600" ]]; then
     echo "FAIL: Private key permissions are $PERMS (expected 600)"
     exit 1
 fi

-DIR_PERMS=$(stat -c "%a" "$SSH_DIR")
+DIR_PERMS=$(get_perms "$SSH_DIR")
 if [[ "$DIR_PERMS" != "700" ]]; then
     echo "FAIL: SSH directory permissions are $DIR_PERMS (expected 700)"
     exit 1
 fi
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PERMS=$(stat -c "%a" "$KEY_FILE")
if [[ "$PERMS" != "600" ]]; then
echo "FAIL: Private key permissions are $PERMS (expected 600)"
exit 1
fi
DIR_PERMS=$(stat -c "%a" "$SSH_DIR")
if [[ "$DIR_PERMS" != "700" ]]; then
echo "FAIL: SSH directory permissions are $DIR_PERMS (expected 700)"
exit 1
get_perms() {
if stat -c "%a" /dev/null &>/dev/null 2>&1; then
stat -c "%a" "$1"
else
stat -f "%Lp" "$1"
fi
}
PERMS=$(get_perms "$KEY_FILE")
if [[ "$PERMS" != "600" ]]; then
echo "FAIL: Private key permissions are $PERMS (expected 600)"
exit 1
fi
DIR_PERMS=$(get_perms "$SSH_DIR")
if [[ "$DIR_PERMS" != "700" ]]; then
echo "FAIL: SSH directory permissions are $DIR_PERMS (expected 700)"
exit 1
fi
πŸ€– Prompt for AI Agents
In `@tests/test_ssh_creation.sh` around lines 65 - 74, The test uses Linux-only
stat flags to check file perms (PERMS=$(stat -c "%a" "$KEY_FILE") and
DIR_PERMS=$(stat -c "%a" "$SSH_DIR")), which will fail on macOS; modify the
permission-check logic in tests/test_ssh_creation.sh to be portable by detecting
the platform (e.g., via uname) and using macOS-compatible stat -f "%Lp" for
KEY_FILE and SSH_DIR or fallback to a shell-based method (e.g., printf "%o"
"$(stat -f "%Lp" ...)" or using ls -l parsing) so the checks for KEY_FILE and
SSH_DIR permissions work on both Linux and macOS. Ensure the permission
variables PERMS and DIR_PERMS are set using the appropriate branch and keep the
existing comparison logic that expects "600" for KEY_FILE and "700" for SSH_DIR.

fi

echo "PASS: SSH key creation secure"
rm -rf "$TEST_HOME"
Comment on lines +60 to +78
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Temp directory leaked on test failure.

If any assertion fails (lines 61–74), the script exits before rm -rf "$TEST_HOME" on line 78. Use a trap to guarantee cleanup.

Suggested fix

Add near the top of the script (after line 5):

 TEST_HOME=$(mktemp -d)
+trap 'rm -rf "$TEST_HOME"' EXIT

Then you can remove the explicit rm -rf at line 78.

πŸ€– Prompt for AI Agents
In `@tests/test_ssh_creation.sh` around lines 60 - 78, Tests currently leak the
temporary directory on failures because the cleanup rm -rf "$TEST_HOME" runs
only on success; add a trap early in the script (after initial variable setup)
that ensures TEST_HOME is removed on EXIT (or on ERR/INT/TERM) so cleanup always
runs, then remove the explicit rm -rf "$TEST_HOME" at the end; reference the
TEST_HOME, KEY_FILE, and SSH_DIR variables when adding the trap to guarantee
removal regardless of which assertion fails.

13 changes: 9 additions & 4 deletions tools/setup-ssh-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,17 @@ cmd_restore() {

say "Restoring SSH key from 1Password..."

# Create SSH directory
mkdir -p "$SSH_DIR"
# Create SSH directory securely
(umask 077 && mkdir -p "$SSH_DIR")
# Redundant chmod for defense in depth
chmod 700 "$SSH_DIR"

# Read private key from 1Password and save locally
op read "op://$VAULT/$KEY_NAME/private_key" > "$PRIVATE_KEY_FILE"
# Read private key from 1Password and save locally securely
(
umask 077
op read "op://$VAULT/$KEY_NAME/private_key" > "$PRIVATE_KEY_FILE"
)
# Redundant chmod for defense in depth
chmod 600 "$PRIVATE_KEY_FILE"

# Read public key from 1Password and save locally
Expand Down
Loading