Skip to content

Commit a7e334f

Browse files
wolfsshd: reject unsafe ownership/symlink on trust-anchor file loads
1 parent e2bfa19 commit a7e334f

6 files changed

Lines changed: 318 additions & 9 deletions

File tree

.github/workflows/paramiko-sftp-test.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,12 @@ jobs:
9494
Subsystem sftp internal-sftp
9595
EOF
9696
97-
# Set proper permissions for keys
97+
# wolfSSHd refuses to load a host key unless it is owned by root or
98+
# the daemon's user and is not group or world writable. The daemon is
99+
# launched with sudo (euid 0) while the checkout key is owned by the
100+
# runner user, so make the key root-owned and 0600.
98101
chmod 600 ./keys/server-key.pem
102+
sudo chown 0:0 ./keys/server-key.pem
99103
100104
# Print debug info
101105
echo "Contents of sshd_config.txt:"

.github/workflows/sshd-test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ jobs:
107107
sudo apt-get -y install valgrind
108108
touch sshd_config.txt
109109
./configure --enable-all LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI -DWOLFSSH_NO_SFTP_TIMEOUT -DWOLFSSH_MAX_SFTP_RW=4000000 -DMAX_PATH_SZ=120" --enable-static --disable-shared && make
110+
# wolfSSHd refuses a host key not owned by root or the daemon's user.
111+
# The daemon runs under sudo (euid 0), so make the key root-owned. Mode
112+
# stays 644 (not group/world writable) so other steps can still read it.
113+
sudo chown 0:0 ./keys/server-key.pem
110114
sudo timeout --preserve-status -s 2 5 valgrind --error-exitcode=1 --leak-check=full ./apps/wolfsshd/wolfsshd -D -f sshd_config -h ./keys/server-key.pem -d -p 22222
111115
112116
# regression test, check that cat command does not hang
@@ -119,6 +123,8 @@ jobs:
119123
cat ./keys/hansel-*.pub > authorized_keys_test
120124
sed -i.bak "s/hansel/$USER/" ./authorized_keys_test
121125
./configure --enable-all LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI -DWOLFSSH_NO_SFTP_TIMEOUT -DWOLFSSH_MAX_SFTP_RW=4000000 -DMAX_PATH_SZ=120" --enable-static --disable-shared && make
126+
# Host key must be root-owned for the sudo-launched daemon to load it.
127+
sudo chown 0:0 ./keys/server-key.pem
122128
sudo ./apps/wolfsshd/wolfsshd -f sshd_config.txt -h ./keys/server-key.pem -p 22225
123129
chmod 600 ./keys/hansel-key-rsa.pem
124130
tail -c 50000 /dev/urandom > test

.github/workflows/x509-interop.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ jobs:
158158
- name: Start wolfSSHd
159159
working-directory: ./wolfssh/
160160
run: |
161+
# wolfSSHd refuses a trust anchor not owned by root or the daemon's
162+
# user. The daemon runs under sudo (euid 0), so make the host key,
163+
# host cert, and user CA root-owned. Modes stay non-group/world
164+
# writable so other steps can still read them.
165+
sudo chown 0:0 ./keys/server-key.pem ./keys/server-cert.pem \
166+
./keys/ca-cert-ecc.pem
161167
sudo ./apps/wolfsshd/wolfsshd -f sshd_config -d \
162168
-E $PWD/wolfsshd-log.txt &
163169
for i in $(seq 1 20); do

apps/wolfsshd/test/run_all_sshd_tests.sh

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,137 @@ else
8383
fi
8484
fi
8585

86+
# Self-contained check for the ownership and symlink gate in getBufferFromFile().
87+
# The host key, host certificate, and user CA all load through the same
88+
# getBufferFromFile(..., 1) call, so exercising the gate via the host key covers
89+
# the identical code for the other two trust anchors. Starts a private wolfSSHd
90+
# with substituted host keys and asserts startup is refused for a symlink, a
91+
# group/world-writable file, and (when run as a non-root user with sudo) a file
92+
# owned by another user, and accepted for a proper mode-600 regular file. Does
93+
# not use the shared daemon, so it runs the same whether or not one was started.
94+
run_hostkey_perm_check() {
95+
printf "host key ownership/symlink gate ... "
96+
TOTAL=$((TOTAL+1))
97+
98+
HK_SSHD=../wolfsshd
99+
HK_KEY=../../../keys/server-key.pem
100+
HK_PORT=22399
101+
if [ ! -x "$HK_SSHD" ] || [ ! -f "$HK_KEY" ]; then
102+
printf "SKIPPED\n"
103+
SKIPPED=$((SKIPPED+1))
104+
return
105+
fi
106+
107+
HK_WORK=$(mktemp -d 2>/dev/null) || HK_WORK=$(mktemp -d -t sshdperm)
108+
if [ -z "$HK_WORK" ] || [ ! -d "$HK_WORK" ]; then
109+
printf "SKIPPED (mktemp failed)\n"
110+
SKIPPED=$((SKIPPED+1))
111+
return
112+
fi
113+
114+
cp "$HK_KEY" "$HK_WORK/hostkey.pem" || {
115+
printf "SKIPPED (could not prepare hostkey)\n"
116+
SKIPPED=$((SKIPPED+1))
117+
rm -rf "$HK_WORK"
118+
return
119+
}
120+
chmod 600 "$HK_WORK/hostkey.pem"
121+
touch "$HK_WORK/authorized_keys"
122+
hk_cfg() {
123+
cat > "$HK_WORK/cfg" <<EOF
124+
Port $HK_PORT
125+
Protocol 2
126+
PermitRootLogin yes
127+
PasswordAuthentication yes
128+
UsePrivilegeSeparation no
129+
UseDNS no
130+
HostKey $1
131+
AuthorizedKeysFile $HK_WORK/authorized_keys
132+
EOF
133+
}
134+
135+
# Load happens during startup before the listener; start, poll the log
136+
# rather than sleeping a fixed time, then stop. Both the host key load and
137+
# the listener emit a line, so stop as soon as either appears (max ~15s).
138+
# $1 (optional): "sudo" to launch the daemon as root for the owner branch.
139+
hk_run() {
140+
HK_PRE="$1"
141+
$HK_PRE "$HK_SSHD" -D -d -f "$HK_WORK/cfg" -p $HK_PORT > "$HK_WORK/log.txt" 2>&1 &
142+
HK_PID=$!
143+
i=0
144+
while [ $i -lt 15 ]; do
145+
if grep -qE "Listening on port|Refusing to load" "$HK_WORK/log.txt" 2>/dev/null; then
146+
break
147+
fi
148+
sleep 1
149+
i=$((i+1))
150+
done
151+
# When launched via sudo, $HK_PID is the sudo pid, not the daemon, and
152+
# sudo does not reliably forward the signal, so match the daemon by port.
153+
# This guarantees a regression cannot leave a root daemon bound to it.
154+
if [ -n "$HK_PRE" ]; then
155+
$HK_PRE pkill -f "$HK_SSHD.*$HK_PORT" 2>/dev/null
156+
else
157+
kill $HK_PID 2>/dev/null
158+
fi
159+
wait $HK_PID 2>/dev/null
160+
}
161+
162+
hk_fail() {
163+
printf "FAILED!\n%s\n" "$1"
164+
cat "$HK_WORK/log.txt"
165+
rm -rf "$HK_WORK"
166+
if [ "$USING_LOCAL_HOST" == 1 ]; then
167+
printf "Shutting down test wolfSSHd\n"
168+
stop_wolfsshd
169+
fi
170+
exit 1
171+
}
172+
173+
# proper mode-600 regular file must load. The only gate failure here is the
174+
# daemon refusing a properly-owned key; any other reason the daemon does not
175+
# reach the listener (port in use, environment cannot run the daemon) is
176+
# unrelated to the gate, so skip rather than fail the whole suite.
177+
hk_cfg "$HK_WORK/hostkey.pem"; hk_run
178+
if grep -q "Refusing to load" "$HK_WORK/log.txt"; then
179+
hk_fail "valid host key was refused"
180+
fi
181+
if ! grep -q "Listening on port" "$HK_WORK/log.txt"; then
182+
printf "SKIPPED (daemon could not listen)\n"
183+
SKIPPED=$((SKIPPED+1))
184+
rm -rf "$HK_WORK"
185+
return
186+
fi
187+
188+
# symlink must be refused
189+
ln -s "$HK_WORK/hostkey.pem" "$HK_WORK/link.pem"
190+
hk_cfg "$HK_WORK/link.pem"; hk_run
191+
grep -q "Refusing to load" "$HK_WORK/log.txt" || hk_fail "symlinked host key was not refused"
192+
193+
# non-regular file (FIFO) must be refused. Skip where mkfifo is unavailable.
194+
if mkfifo "$HK_WORK/fifo.pem" 2>/dev/null; then
195+
hk_cfg "$HK_WORK/fifo.pem"; hk_run
196+
grep -q "Refusing to load" "$HK_WORK/log.txt" || hk_fail "FIFO host key was not refused"
197+
fi
198+
199+
# group/world-writable file must be refused
200+
cp "$HK_KEY" "$HK_WORK/ww.pem"; chmod 666 "$HK_WORK/ww.pem"
201+
hk_cfg "$HK_WORK/ww.pem"; hk_run
202+
grep -q "Refusing to load" "$HK_WORK/log.txt" || hk_fail "world-writable host key was not refused"
203+
204+
# Owner-rejection branch (st_uid != 0 && st_uid != geteuid()): the primary
205+
# substitution vector. The mode-600 host key is owned by the invoking user,
206+
# so launching the daemon as root (euid 0) must refuse it. Needs a non-root
207+
# invoker and non-interactive sudo; skip the sub-case otherwise.
208+
if [ "`id -u`" -ne 0 ] && sudo -n true 2>/dev/null; then
209+
hk_cfg "$HK_WORK/hostkey.pem"; hk_run sudo
210+
grep -q "Refusing to load" "$HK_WORK/log.txt" || hk_fail "non-root-owned host key was not refused under root daemon"
211+
fi
212+
213+
rm -rf "$HK_WORK"
214+
printf "PASSED\n"
215+
}
216+
86217
run_test() {
87218
printf "$1 ... "
88219
./"$1" "$TEST_HOST" "$TEST_PORT" "$USER" &> stdout.txt
@@ -137,6 +268,8 @@ else
137268
# add additional tests here, check on var USING_LOCAL_HOST if can make sshd
138269
# server start/restart with changes
139270

271+
run_hostkey_perm_check
272+
140273
if [ "$USING_LOCAL_HOST" == 1 ]; then
141274
printf "Shutting down test wolfSSHd\n"
142275
stop_wolfsshd

apps/wolfsshd/test/start_sshd.sh

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,82 @@
11
#!/bin/bash
22

3+
# Holds the per-daemon temp dir used for root-owned trust-anchor copies, so
4+
# stop_wolfsshd can remove it. Empty when no copies were made.
5+
SSHD_KEYDIR=""
6+
37
# starts up a sshd session, takes in the sshd_config file as an argument
48
start_wolfsshd() {
59
CURRENT_PIDS=`ps -e | grep wolfsshd | grep -oE "[0-9]+"`
10+
11+
ORIGCFG="$1"
12+
CONFIG="$ORIGCFG"
13+
# Reset so each invocation is self-contained regardless of call ordering.
14+
SSHD_KEYDIR=""
15+
16+
# wolfSSHd refuses to load a trust-anchor file that is not owned by the
17+
# daemon's user. This shared daemon is launched with sudo (euid 0) while the
18+
# repository key files are owned by the checkout user, so copy each
19+
# configured host key, host cert, and user CA into a private dir, make the
20+
# copies root-owned, and emit a temp config pointing at them. The
21+
# version-controlled files are left untouched so the suite stays re-runnable.
22+
if grep -qE '^(HostKey|HostCertificate|TrustedUserCAKeys)[[:space:]]' "$ORIGCFG"; then
23+
SSHD_KEYDIR=$(mktemp -d 2>/dev/null) || SSHD_KEYDIR=$(mktemp -d -t sshdkeys)
24+
if [ -z "$SSHD_KEYDIR" ] || [ ! -d "$SSHD_KEYDIR" ]; then
25+
printf "WARNING: could not create temp dir for trust-anchor copies; using original config\n" >&2
26+
SSHD_KEYDIR=""
27+
else
28+
CONFIG="$SSHD_KEYDIR/sshd_config"
29+
: > "$CONFIG" || { printf "WARNING: could not write %s; using original config\n" "$CONFIG" >&2; CONFIG="$ORIGCFG"; rm -rf "$SSHD_KEYDIR"; SSHD_KEYDIR=""; }
30+
fi
31+
# Only rewrite when the temp config was set up. On any fallback above
32+
# SSHD_KEYDIR is empty and CONFIG still points at ORIGCFG; running the
33+
# loop then would read from and append to the same file, never reaching
34+
# EOF (runaway append) and would also operate on "/anchorN.pem" at the
35+
# filesystem root. Skipping it leaves the original config untouched.
36+
if [ -n "$SSHD_KEYDIR" ]; then
37+
n=0
38+
# Rewrite the config line by line. For each trust-anchor directive
39+
# copy the file to a counter-named destination (so distinct
40+
# directories with the same basename do not collide) and emit the
41+
# directive pointing at the copy. Paths are built by string assembly,
42+
# not sed, so a checkout path containing regex or glob metacharacters
43+
# cannot corrupt the rewrite. The directive keyword is the first
44+
# field and the path is the remainder, so a path containing spaces is
45+
# preserved. The "|| [ -n "$line" ]" keeps a final line lacking a
46+
# trailing newline from being dropped.
47+
while IFS= read -r line || [ -n "$line" ]; do
48+
read -r key src <<EOF
49+
$line
50+
EOF
51+
case "$key" in
52+
HostKey|HostCertificate|TrustedUserCAKeys)
53+
if [ -n "$src" ] && [ -e "$src" ]; then
54+
n=`expr $n + 1`
55+
dst="$SSHD_KEYDIR/anchor$n.pem"
56+
if ! cp "$src" "$dst"; then
57+
printf "WARNING: could not copy %s; using original path\n" "$src" >&2
58+
printf '%s\n' "$line" >> "$CONFIG"
59+
continue
60+
fi
61+
chmod go-w "$dst"
62+
if ! sudo chown 0 "$dst"; then
63+
printf "WARNING: could not chown %s to root; daemon may refuse to load it\n" "$src" >&2
64+
fi
65+
printf '%s %s\n' "$key" "$dst" >> "$CONFIG"
66+
else
67+
printf '%s\n' "$line" >> "$CONFIG"
68+
fi
69+
;;
70+
*)
71+
printf '%s\n' "$line" >> "$CONFIG"
72+
;;
73+
esac
74+
done < "$ORIGCFG"
75+
fi
76+
fi
77+
678
# find a port
7-
sudo ../wolfsshd -d -E ./log.txt -f $1
79+
sudo ../wolfsshd -d -E ./log.txt -f "$CONFIG"
880

981
# set the PID of started sshd
1082
NEW_PID=`ps -e | grep wolfsshd | grep -oE "[0-9]+"`
@@ -16,4 +88,11 @@ start_wolfsshd() {
1688
stop_wolfsshd() {
1789
printf "Stopping SSHD, killing pid $PID\n"
1890
sudo kill $PID
91+
92+
# The temp dir is owned by the invoking user, so its root-owned key copies
93+
# can be removed without sudo.
94+
if [ -n "$SSHD_KEYDIR" ]; then
95+
rm -rf "$SSHD_KEYDIR"
96+
SSHD_KEYDIR=""
97+
fi
1998
}

0 commit comments

Comments
 (0)