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 @@
## 2024-02-10 - [SSH Key Creation Race Condition]

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: "## 2024-02-10 - [SSH Key Creat..."] 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: "## 2024-02-10 - [SSH Key Creation Race Condition]"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md
**Vulnerability:** SSH private keys were briefly world-readable during creation because `umask` was not restricted. The `op read ... > file` command created the file with default permissions (often 644/664), and `chmod 600` was only applied afterward.

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: 251] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md
**Learning:** Shell redirection creates files before `chmod` runs. This creates a Time-of-Check to Time-of-Use (TOCTOU) window where sensitive data is exposed.

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: 159] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md
**Prevention:** Always wrap sensitive file creation commands in a subshell with restricted umask: `(umask 077; command > sensitive_file)`. This ensures the file is born secure.

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: 176] 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 failures flagged by CI and correct the year.

  1. The date reads 2024-02-10 — should be 2026-02-10.
  2. CI reports several markdownlint violations: missing top-level heading (MD041), no blank line after heading (MD022), and lines exceeding 80 characters (MD013).
Proposed fix
-## 2024-02-10 - [SSH Key Creation Race Condition]
-**Vulnerability:** SSH private keys were briefly world-readable during creation because `umask` was not restricted. The `op read ... > file` command created the file with default permissions (often 644/664), and `chmod 600` was only applied afterward.
-**Learning:** Shell redirection creates files before `chmod` runs. This creates a Time-of-Check to Time-of-Use (TOCTOU) window where sensitive data is exposed.
-**Prevention:** Always wrap sensitive file creation commands in a subshell with restricted umask: `(umask 077; command > sensitive_file)`. This ensures the file is born secure.
+# Sentinel Journal
+
+## 2026-02-10 - SSH Key Creation Race Condition
+
+**Vulnerability:** SSH private keys were briefly world-readable during
+creation because `umask` was not restricted. The `op read ... > file`
+command created the file with default permissions (often 644/664), and
+`chmod 600` was only applied afterward.
+
+**Learning:** Shell redirection creates files before `chmod` runs. This
+creates a TOCTOU window where sensitive data is exposed.
+
+**Prevention:** Always wrap sensitive file creation in a subshell with
+restricted umask: `(umask 077; command > sensitive_file)`.
📝 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
## 2024-02-10 - [SSH Key Creation Race Condition]
**Vulnerability:** SSH private keys were briefly world-readable during creation because `umask` was not restricted. The `op read ... > file` command created the file with default permissions (often 644/664), and `chmod 600` was only applied afterward.
**Learning:** Shell redirection creates files before `chmod` runs. This creates a Time-of-Check to Time-of-Use (TOCTOU) window where sensitive data is exposed.
**Prevention:** Always wrap sensitive file creation commands in a subshell with restricted umask: `(umask 077; command > sensitive_file)`. This ensures the file is born secure.
# Sentinel Journal
## 2026-02-10 - SSH Key Creation Race Condition
**Vulnerability:** SSH private keys were briefly world-readable during
creation because `umask` was not restricted. The `op read ... > file`
command created the file with default permissions (often 644/664), and
`chmod 600` was only applied afterward.
**Learning:** Shell redirection creates files before `chmod` runs. This
creates a TOCTOU window where sensitive data is exposed.
**Prevention:** Always wrap sensitive file creation in a subshell with
restricted umask: `(umask 077; command > sensitive_file)`.
🧰 Tools
🪛 GitHub Check: Lint Documentation

[failure] 4-4: Line length
.jules/sentinel.md:4:81 MD013/line-length Line length [Expected: 80; Actual: 176] 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: 159] 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: 251] 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: "## 2024-02-10 - [SSH Key Creat..."] 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: "## 2024-02-10 - [SSH Key Creation Race Condition]"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md

🤖 Prompt for AI Agents
In @.jules/sentinel.md around lines 1 - 4, Update the entry header and fix
markdownlint issues: change the heading "## 2024-02-10 - [SSH Key Creation Race
Condition]" to a top-level heading and correct the date to "2026-02-10" (e.g. "#
2026-02-10 - [SSH Key Creation Race Condition]"), ensure there is a blank line
after that heading, and reflow any lines longer than 80 characters (split the
"Vulnerability", "Learning", and "Prevention" lines into shorter
sentences/paragraphs) so the umask subshell recommendation "(umask 077; command
> sensitive_file)" and other text comply with MD013, MD022, and MD041.

1 change: 1 addition & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ get_shell_scripts() {
"$ROOT_DIR/scripts"
"$ROOT_DIR/scripts/backup"
"$ROOT_DIR/cron"
"$ROOT_DIR/tests"
"$ROOT_DIR"
)

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

# Setup test environment
TEST_DIR=$(mktemp -d)
Comment on lines +1 to +5
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

Add a trap to ensure cleanup on failure.

With set -e, if any assertion fails the script exits immediately and rm -rf "$TEST_DIR" on line 85 is never reached, leaking temp directories.

Proposed fix
 #!/bin/bash
 set -e
 
 # Setup test environment
 TEST_DIR=$(mktemp -d)
+trap 'rm -rf "$TEST_DIR"' EXIT
 export HOME="$TEST_DIR"

And remove the manual cleanup at the end:

-# Cleanup
-rm -rf "$TEST_DIR"

Also applies to: 84-85

🤖 Prompt for AI Agents
In `@tests/test_ssh_creation.sh` around lines 1 - 5, Add a cleanup function that
removes TEST_DIR and register it with trap so it runs on exit/failure (e.g.,
define cleanup() { rm -rf "$TEST_DIR"; } and call trap cleanup EXIT or trap
cleanup ERR EXIT) to ensure temp dirs are removed when set -e causes early exit;
reference the TEST_DIR variable and remove the manual rm -rf "$TEST_DIR" at the
end of the script so the trap handles cleanup consistently.

export HOME="$TEST_DIR"
export XDG_CONFIG_HOME="$TEST_DIR/.config"
export XDG_DATA_HOME="$TEST_DIR/.local/share"
mkdir -p "$TEST_DIR/.config/dotfiles"
mkdir -p "$TEST_DIR/.local/bin"

# Add mock bin to PATH
export PATH="$TEST_DIR/.local/bin:$PATH"

# Create mock op
cat <<EOF > "$TEST_DIR/.local/bin/op"
#!/bin/bash
if [[ "\$1" == "account" && "\$2" == "list" ]]; then
exit 0
elif [[ "\$1" == "item" && "\$2" == "get" ]]; then
exit 0
elif [[ "\$1" == "read" ]]; then
if [[ "\$2" == *"private_key"* ]]; then
echo "MOCK_PRIVATE_KEY_CONTENT"
else
echo "MOCK_PUBLIC_KEY_CONTENT"
fi
else
exit 0
fi
EOF
chmod +x "$TEST_DIR/.local/bin/op"

# Create mock yq (script uses it if available)
cat <<EOF > "$TEST_DIR/.local/bin/yq"
#!/bin/bash
echo "" # Return empty or default
EOF
chmod +x "$TEST_DIR/.local/bin/yq"

echo "Running setup-ssh-keys.sh restore in test environment..."

# Determine absolute path to the tool
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
TOOL_PATH="$REPO_ROOT/tools/setup-ssh-keys.sh"

# Run the tool
# Pass --vault and --name to avoid yq dependency issues or defaults
bash "$TOOL_PATH" restore --vault "testvault" --name "testkey"

# Verify permissions
PRIVATE_KEY="$HOME/.ssh/id_ed25519"

if [[ ! -f "$PRIVATE_KEY" ]]; then
echo "ERROR: Private key not created at $PRIVATE_KEY"
ls -la "$HOME/.ssh"
exit 1
fi

PERMS=$(stat -c "%a" "$PRIVATE_KEY" 2>/dev/null || stat -f "%Lp" "$PRIVATE_KEY")

echo "Permissions of private key: $PERMS"

if [[ "$PERMS" != "600" ]]; then
echo "ERROR: Permissions are not 600!"
exit 1
fi

echo "SUCCESS: Key created with correct permissions."

# Verify directory permissions
SSH_DIR="$HOME/.ssh"
DIR_PERMS=$(stat -c "%a" "$SSH_DIR" 2>/dev/null || stat -f "%Lp" "$SSH_DIR")
echo "Permissions of .ssh directory: $DIR_PERMS"

if [[ "$DIR_PERMS" != "700" ]]; then
echo "ERROR: Directory permissions are not 700!"
exit 1
fi

echo "SUCCESS: Directory created with correct permissions."

# Cleanup
rm -rf "$TEST_DIR"
7 changes: 5 additions & 2 deletions tools/setup-ssh-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,14 @@ cmd_restore() {
say "Restoring SSH key from 1Password..."

# Create SSH directory
mkdir -p "$SSH_DIR"
(umask 077 && mkdir -p "$SSH_DIR")
chmod 700 "$SSH_DIR"

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

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