Skip to content

🐛 Fix sshd sshd config check (globPathPattern used as file)#6318

Merged
Bajusz15 merged 12 commits intomainfrom
mate/sshd_security_check
Jan 13, 2026
Merged

🐛 Fix sshd sshd config check (globPathPattern used as file)#6318
Bajusz15 merged 12 commits intomainfrom
mate/sshd_security_check

Conversation

@Bajusz15
Copy link
Copy Markdown
Contributor

@Bajusz15 Bajusz15 commented Dec 18, 2025

Issue: sshd config parser stores file patterns in Context.Path (as path), causing "file not found" errors, because the pattern is treated as a file.

When parsing sshd configuration files that use Include directives with glob patterns (e.g., Include /etc/ssh/sshd_config.d/.conf), the parser was storing the literal glob pattern string in the MatchBlock.Context.Path field instead of the actual expanded file paths.
Because of this:
When sshd.config.blocks is accessed, matchBlocks2Resources() attempts to create file resources using cur.Context.Path, since Context.Path contained the literal glob pattern (e.g., /etc/ssh/sshd_config.d/
.conf) instead of the actual file path (e.g., /etc/ssh/sshd_config.d/01-mondoo.conf), it resulted in errors like:
file '/etc/ssh/sshd_config.d/*.conf' not found

Root Cause:
ParseBlocks() was setting Context.Path to the rootPath parameter (which could be a glob pattern) without tracking which actual files were expanded
expandGlob() returned an error when directories didn't exist, instead of treating it as "no matches" (standard glob behavior)

Example:
SSHD config contains: Include /etc/ssh/sshd_config.d/.conf
Directory exists with file: /etc/ssh/sshd_config.d/01-mondoo.conf
Parser creates MatchBlock with Context.Path = "/etc/ssh/sshd_config.d/
.conf" (literal glob)

How to reproduce:
Run the following script:

#!/bin/bash
# Test script to reproduce the exact customer scenario
# Customer has /etc/ssh/sshd_config.d/01-mondoo.conf with their config
# The error occurs when running the specific MQL queries

set -e

# Get the script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Set up logging
LOG_FILE="$SCRIPT_DIR/test-customer-scenario-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1

echo "=== Customer Scenario Test ==="
echo "Log file: $LOG_FILE"
echo ""

# Build cnquery for Linux
echo "=== Building cnquery for Linux ==="
CNQUERY_BIN="$SCRIPT_DIR/cnquery-linux"
if [ ! -f "$CNQUERY_BIN" ]; then
    echo "Building cnquery for Linux (amd64)..."
    cd "$SCRIPT_DIR"
    GOOS=linux GOARCH=amd64 go build -o "$CNQUERY_BIN" ./apps/cnquery
    if [ $? -ne 0 ]; then
        echo "ERROR: Failed to build cnquery"
        exit 1
    fi
    echo "✓ Built cnquery for Linux"
else
    echo "✓ Using existing Linux binary"
fi
echo ""

# Create test directory
TEST_DIR=$(mktemp -d)
echo "Test directory: $TEST_DIR"
cd "$TEST_DIR"

# Copy binary
cp "$CNQUERY_BIN" ./cnquery-linux

# Create Dockerfile with customer's exact scenario
cat > Dockerfile << 'EOF'
FROM ubuntu:22.04

# Install OpenSSH server
RUN apt-get update && \
    apt-get install -y openssh-server && \
    mkdir -p /var/run/sshd

# Create the directory and customer's exact config file
RUN mkdir -p /etc/ssh/sshd_config.d && \
    echo "KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512" > /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "MACs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "HostbasedAuthentication no" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "PermitEmptyPasswords no" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "PermitUserEnvironment no" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "PermitRootLogin no" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "ClientAliveInterval 300" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "ClientAliveCountMax 0" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "LoginGraceTime 60" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "MaxAuthTries 4" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "IgnoreRhosts yes" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "LogLevel VERBOSE" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "X11Forwarding no" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "Banner /etc/issue.net" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "AllowGroups ssh_users" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "Match all" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    echo "  ClientAliveCountMax 3" >> /etc/ssh/sshd_config.d/01-mondoo.conf && \
    chmod 644 /etc/ssh/sshd_config.d/01-mondoo.conf

# Ensure Include directive is in main config
RUN grep -q "Include /etc/ssh/sshd_config.d/\*\.conf" /etc/ssh/sshd_config || \
    echo "Include /etc/ssh/sshd_config.d/*.conf" >> /etc/ssh/sshd_config

# Copy cnquery binary
COPY cnquery-linux /usr/local/bin/cnquery
RUN chmod +x /usr/local/bin/cnquery

# Keep container running
CMD ["/bin/bash", "-c", "while true; do sleep 3600; done"]
EOF

echo "=== Building Docker image ==="
docker build -t test-customer-scenario . 2>&1 | tail -10

CONTAINER=$(docker run -d test-customer-scenario)
echo "Container ID: $CONTAINER"
echo ""

echo "=== Verifying setup ==="
docker exec "$CONTAINER" test -f /etc/ssh/sshd_config.d/01-mondoo.conf && echo "✓ Config file exists" || echo "✗ Config file missing"
docker exec "$CONTAINER" grep -q "Include /etc/ssh/sshd_config.d/\*\.conf" /etc/ssh/sshd_config && echo "✓ Include directive found" || echo "✗ Include directive missing"
echo ""

echo "=== Running customer's exact failing query ==="
echo "Query:"
echo "  defaultBlock = sshd.config.blocks.where(criteria == \"\");"
echo "  userBlocks = sshd.config.blocks.where(criteria != \"\");"
echo "  userBlocks.all(params.ClientAliveInterval >= 1);"
echo "  defaultBlock.all(params.ClientAliveInterval >= 1);"
echo ""

# Simplified query without props (since we don't have props defined)
QUERY="defaultBlock = sshd.config.blocks.where(criteria == \"\"); userBlocks = sshd.config.blocks.where(criteria != \"\"); userBlocks.all(params.ClientAliveInterval >= 1); defaultBlock.all(params.ClientAliveInterval >= 1); defaultBlock; userBlocks"

docker exec "$CONTAINER" cnquery run local -c "$QUERY" 2>&1 | tee /tmp/customer-query-output.log
	


PS. Since then, it was discovered for docker connection some file system methods were not supported, so I added implementations.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 18, 2025

Test Results

4 668 tests  +2   4 664 ✅ +2   2m 13s ⏱️ -3s
  415 suites ±0       4 💤 ±0 
   31 files   ±0       0 ❌ ±0 

Results for commit b7c140e. ± Comparison against base commit 5349f0e.

♻️ This comment has been updated with latest results.

@Bajusz15 Bajusz15 marked this pull request as ready for review December 18, 2025 16:26
@Bajusz15 Bajusz15 self-assigned this Jan 6, 2026
// This ensures that glob patterns use the actual file path instead of the glob pattern
actualPath := rootPath
if len(actualPaths) > 0 {
actualPath = actualPaths[0]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about the other matches?

Taking the example from your PR description, let's assume the glob *.conf would match 01_mondoo.conf and 02_mondoo.conf.

From reading the code, I think we would not detect the 2nd file.

What do you think about expanding the glob outside this function, and calling the function for every file found?

@@ -15,8 +15,8 @@ func TestSSHParser(t *testing.T) {
raw, err := os.ReadFile("./testdata/sshd_config")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be possible to expand the testdata and split the single file up into at least two includes in subdirectories?

This way, we could add the glob case to the test cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@czunker I changed the approach now, and added test for multiple includes. I also verified using the script to reproduce. When I rebuild cnquery with my changes, the problem is gone.

@czunker
Copy link
Copy Markdown
Contributor

czunker commented Jan 9, 2026

I tried it with a container, but couldn't parse the config:

cnquery run docker 14df8ca5f2d0 -c 'sshd.config.blocks.where(criteria != "")'
→ no Mondoo configuration file provided, using defaults
! unable to expand Include directive error="not implemented" path=/etc/ssh/sshd_config.d/*.conf
sshd.config.blocks.where: []

The file layout looks like this:

./sshd_config.d
./sshd_config.d/10-mondoo.conf
./sshd_config.d/50-redhat.conf
./sshd_config.d/20-mondoo.conf
./sshd_config.d/40-redhat-crypto-policies.conf
./sshd_config

The sshd config has an Include:

Include /etc/ssh/sshd_config.d/*.conf

@czunker
Copy link
Copy Markdown
Contributor

czunker commented Jan 9, 2026

I checked the config. One of the included files has another Include directive:

[root@14df8ca5f2d0 ssh]# grep Include * -R
sshd_config:Include /etc/ssh/sshd_config.d/*.conf
sshd_config.d/40-redhat-crypto-policies.conf:Include /etc/crypto-policies/back-ends/opensshserver.config

I commented that one out, but no change.

@Bajusz15
Copy link
Copy Markdown
Contributor Author

cnquery run docker 14df8ca5f2d0 -c 'sshd.config.blocks.where(criteria != "")'

I think the unimplemented error here is because you are running it via docker connection, not locally, and the method is not implemented. But I will make a fix for this too as it's a valid use case.

@Bajusz15
Copy link
Copy Markdown
Contributor Author

@czunker I got same not implemented error when checking with docker connection. I added implementation to the missing method.
Can you try again with cnquery run docker 14df8ca5f2d0 -c 'sshd.config.blocks.where(criteria != "")'?

Comment on lines +99 to +105
var statPath string
if filepath.IsAbs(name) {
statPath = name
} else {
statPath = filepath.Join(f.path, name)
}
res[i], err = f.catfs.Stat(statPath)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This function was not used until now, but I made the change so it's backward compatible.

@czunker
Copy link
Copy Markdown
Contributor

czunker commented Jan 13, 2026

@czunker I got same not implemented error when checking with docker connection. I added implementation to the missing method. Can you try again with cnquery run docker 14df8ca5f2d0 -c 'sshd.config.blocks.where(criteria != "")'?

@Bajusz15 Thanks for adding this to the container connection.

The error is gone, but the blocks are empty:

cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria != "")'
→ no Mondoo configuration file provided, using defaults
sshd.config.blocks.where: []

The default block works:

cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria == "")'
→ no Mondoo configuration file provided, using defaults
sshd.config.blocks.where: [
  0: sshd.config.matchBlock criteria=""
     in /etc/ssh/sshd_config:1-132
...

Anything else I can try?

@Bajusz15
Copy link
Copy Markdown
Contributor Author

@czunker I got same not implemented error when checking with docker connection. I added implementation to the missing method. Can you try again with cnquery run docker 14df8ca5f2d0 -c 'sshd.config.blocks.where(criteria != "")'?

@Bajusz15 Thanks for adding this to the container connection.

The error is gone, but the blocks are empty:

cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria != "")'
→ no Mondoo configuration file provided, using defaults
sshd.config.blocks.where: []

The default block works:

cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria == "")'
→ no Mondoo configuration file provided, using defaults
sshd.config.blocks.where: [
  0: sshd.config.matchBlock criteria=""
     in /etc/ssh/sshd_config:1-132
...

Anything else I can try?

No, I need to investigate what happens here with your example. I was only focusing on the bug this ticket was trying to solve.

@Bajusz15
Copy link
Copy Markdown
Contributor Author

No, I need to investigate what happens here with your example. I was only focusing on the bug this ticket was trying to solve.

I believe this is correct, if there are no Match blocks we return no results:

cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria != "")'
→ no Mondoo configuration file provided, using defaults
sshd.config.blocks.where: []

@czunker
Copy link
Copy Markdown
Contributor

czunker commented Jan 13, 2026

As discussed, I was missing parts of the config.
Adding group names fixes my issue:

cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria != "")'
→ no Mondoo configuration file provided, using defaults
sshd.config.blocks.where: [
  0: sshd.config.matchBlock criteria="group group1"
     in /etc/ssh/sshd_config.d/10-mondoo.conf:1-11
     1:  Match group group1
     2:    KexAlgorithms [sntrup761x25519-sha512@openssh.com](mailto:sntrup761x25519-sha512@openssh.com),...
     ...
     9:    PermitRootLogin no
     10:
  1: sshd.config.matchBlock criteria="group group2"
     in /etc/ssh/sshd_config.d/20-mondoo.conf:1-14
      1:  Match group group2
      2:    ClientAliveInterval 300
     ...
     12:    AllowGroups ssh_users
     13:
]

And I can also query the group:

~/workspace/mondoo/github.com/cnquery mate/sshd_security_check ❯ cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria != "").all(params.ClientAliveInterval >= 1)'
→ no Mondoo configuration file provided, using defaults
[failed] [].all()
  actual:   []
~/workspace/mondoo/github.com/cnquery mate/sshd_security_check ❯ cnquery run docker 67f1fc72c9fd -c 'sshd.config.blocks.where(criteria != "").any(params.ClientAliveInterval >= 1)'
→ no Mondoo configuration file provided, using defaults
[ok] value: true

Copy link
Copy Markdown
Contributor

@czunker czunker left a comment

Choose a reason for hiding this comment

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

Thanks @Bajusz15

@Bajusz15 Bajusz15 merged commit 4d4b9f8 into main Jan 13, 2026
20 checks passed
@Bajusz15 Bajusz15 deleted the mate/sshd_security_check branch January 13, 2026 16:06
@github-actions github-actions bot locked and limited conversation to collaborators Jan 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants