-
Notifications
You must be signed in to change notification settings - Fork 1
π‘οΈ Sentinel: Fix TOCTOU race condition in SSH key creation #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| **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
|
||
| **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
|
||
| **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
|
||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unquoted
- echo "mock-op-called-with: $@" >&2
+ echo "mock-op-called-with: $*" >&2π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The script under test references 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
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "PASS: SSH key creation secure" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rm -rf "$TEST_HOME" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Temp directory leaked on test failure. If any assertion fails (lines 61β74), the script exits before Suggested fixAdd near the top of the script (after line 5): TEST_HOME=$(mktemp -d)
+trap 'rm -rf "$TEST_HOME"' EXITThen you can remove the explicit π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 reads2025-02-11β should this be2026-02-11?Suggested structure
π Committable suggestion
π§° 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