使用成品 NAS,在相对省心的同时,也意味着更低的自由度。很多自定义操作都是在和这些定制化 OS 斗智斗勇。

本文介绍了在 QNAP 威联通 NAS(以及 Synology 群晖 NAS)上关闭 ssh 密码登录,仅允许密钥登录的方法。Synology 相对还好,对 OpenSSH 的魔改不多,和普通 Linux 一样修改 /etc/ssh/sshd_config 就好了。

但是 QNAP 这个 B,每次启动 sshd 服务都会重新生成配置,直接修改上述配置文件是没法持久化保存的。

风险提示

为了避免你被锁在门外,修改自带 sshd 配置之前,最好自己再起一个 ssh 服务,等确认 OK 了之后再关掉。以及最重要的,做好备份!

sudo opkg install dropbear
sudo dropbear -F -p 9922 -P /opt/var/run/dropbear.pid -R -w -E

如果你不幸已经进不去系统了,可以这样自救(别问我为什么知道🤣):

  • 从 Web UI 打开 Container Station / Container Manager
  • 通过上传 docker-compose.yml 的方式创建容器(通常方式创建的话是不让挂载特殊路径的)
  • 挂载要修改的路径,比如 /etc/init.d/
  • 启动容器后,选择 打开终端机/连接终端,然后把改坏的文件改回来
services:
  rescue-tmp:
    image: ubuntu:latest
    container_name: rescue-tmp
    stdin_open: true
    tty: true
    command: /bin/bash
    volumes:
      - /etc/init.d:/work

Synology NAS

改文件之前做好备份!

执行 sudo vim /etc/ssh/sshd_config,修改:

 PubkeyAuthentication yes
 ChallengeResponseAuthentication no

-PasswordAuthentication yes
+PasswordAuthentication no

然后 sudo synosystemctl restart sshd.service 重启 sshd 生效,或者在 DSM Web UI 上关闭「启动 SSH 功能」后再打开也可以。

值得一提的是,直接修改 sshd_config 中的 Port,会导致 sshd 监听在两个端口上,一个是 Web UI 上设置的「SSH 端口」,一个是配置文件中配置的端口,而且后者是无法用于登录 Shell 的,会提示 Permission denied, please try again.

研究了一下,应该是因为 sshd 被魔改过,会读取 /etc/synoinfo.conf 文件里的 ssh_port 配置。

sudo lsof -c sshd
# COMMAND   PID USER  FD   TYPE  SIZE/OFF    NODE NAME
# sshd    28232 root cwd    DIR      4096       2 /
# sshd    28232 root rtd    DIR      4096       2 /
# sshd    28232 root txt    REG    888480   20645 /usr/bin/sshd
# sshd    28232 root   3u  IPv4       0t0     TCP *:2222 (LISTEN)
# sshd    28232 root   4u  IPv6       0t0     TCP *:2222 (LISTEN)
# sshd    28232 root   5u  IPv4       0t0     TCP *:6622 (LISTEN)
# sshd    28232 root   6u  IPv6       0t0     TCP *:6622 (LISTEN)

cat /etc/synoinfo.conf | grep -i port
# ssh_port="2222"
# sftpPort="3322"
# rsync_sshd_port="4422"

strings /usr/bin/sshd
# SYNOServiceSSHPortGet
# /etc/ssh/sshd_config
# /etc/synoinfo.conf
# Permission denied, please try again.

而根据 Reverse Engineering Synology’s OpenSSH 这篇博客里的说法,这个魔改过的 OpenSSH 会校验一些参数,所以无法连接。

不过对于我们的目标「禁用 ssh 密码登录」没什么影响,就这样吧。这些厂商定制化的东西还是少碰,别给整挂了。

QNAP NAS

大的来了(掩鼻)。

先看看 QNAP 的 sshd 用的什么配置文件:

ps aux | grep sshd
# 22714 admin     11504 S   /usr/sbin/sshd -f /etc/config/ssh/sshd_config -p 2222

cat /etc/config/ssh/sshd_config
# Protocol 2
# HostKey /etc/ssh/ssh_host_ed25519_key
# PermitRootLogin yes
# UseDNS no
# Subsystem sftp /usr/libexec/sftp-server
# AllowTcpForwarding yes
# AllowUsers admin prin

echo 'PasswordAuthentication no' | sudo tee -a /etc/config/ssh/sshd_config

重启 sshd 后,你会惊喜地发现,刚才改掉的配置文件,诶,又被改回来了😁:

sudo setsid /etc/init.d/login.sh restart

grep PasswordAuthentication /etc/config/ssh/sshd_config

既然配置文件没法持久化,那就只能从启动脚本下手了。

简单看看 /etc/init.d/login.sh(文件缩进就是这样,我保留了原汁原味):

SSH=/usr/sbin/sshd
SSHD_CONF=/etc/config/ssh/sshd_config
SSHD_CONF_DEFAULT=/etc/ssh/sshd_config
SSH_PORT=`/sbin/getcfg LOGIN "SSH Port" -d 22`

update_sshd_config()
{
 /sbin/mksshdconf
 ENABLED_SFTP=`/sbin/getcfg LOGIN "SFTP Enable" -u -d TRUE`
 # ...
 #Set PermitRootLogin yes
 OPTION="PermitRootLogin"
 if [ -z "`grep .*${OPTION}.* ${SSHD_CONF}`" ]; then
     echo "${OPTION} yes" > ${SSHD_CONF}
 else
        sed -i "s/^#\s\?${OPTION}\s\?[yesno]\{1,3\}.*/${OPTION} yes/g" ${SSHD_CONF}
 fi
 # ...
}

# ...
case "$1" in
    start)
 # for openssh 7.5p1 and later
 sed -i '/^UsePrivilegeSeparation .*/d' ${SSHD_CONF_DEFAULT}

 /bin/chmod 0400 /etc/config/shadow* /etc/default_config/shadow
 update_ssh_client_config
 if [ `/sbin/getcfg LOGIN "SSH Enable" -u -d FALSE` != FALSE ]; then
  echo -n "Starting sshd service: "
  generte_ssh_key
     if [ ! -f "${SSHD_CONF}" ]; then
      /bin/cp -f ${SSHD_CONF_DEFAULT} ${SSHD_CONF}
     fi
     if [ ! -f "${SSHD_CONF}" ]; then
      SSHD_CONF=${SSHD_CONF_DEFAULT}
     fi
  update_sshd_config
        sshd_privilege_separation
  /sbin/daemon_mgr sshd start "$SSH -f ${SSHD_CONF} -p $SSH_PORT"
  echo "OK"
  touch /var/lock/subsys/sshd
 fi
 ;;
 # ...
esac

你可能以为有这个 cp -f ${SSHD_CONF_DEFAULT} ${SSHD_CONF},我们就能通过修改 /etc/ssh/sshd_config 来实现配置持久化了 —— 天真!

实测下来发现 /sbin/mksshdconf 这个二进制每次执行都会重置 /etc/config/ssh/sshd_config 的内容,所以你 cp 了也没有用。我也不知道他们为什么要写一个没有意义的 cp 在那里。🤷

我搜了下,这篇博客 里也有类似的吐槽:

For reasons I can’t fathom, the SSH config on a QNAP seems nuts (If anyone knows why it works this way, please let me know, because I must be missing something obvious).

就算抛开这些不谈,你这个强制 PermitRootLogin yes 是何意啊?

所以我们还是只能改脚本,重启 sshd 后生效:

改文件之前做好备份!

--- /etc/init.d/login.sh.bak
+++ /etc/init.d/login.sh
@@ -83,6 +83,9 @@
 update_sshd_config()
 {
  /sbin/mksshdconf
+ echo 'PasswordAuthentication no' >> ${SSHD_CONF}
+ echo 'ChallengeResponseAuthentication no' >> ${SSHD_CONF}
+ sed -i "s/PermitRootLogin yes/PermitRootLogin no/g" ${SSHD_CONF}
  ENABLED_SFTP=`/sbin/getcfg LOGIN "SFTP Enable" -u -d TRUE`

  if [ "x${ENABLED_SFTP}" = "xTRUE" ]; then

注意:系统更新后这个脚本会被重置,需要重新修改。

Bonus: macOS

其实和大部分 Linux 一样啦,写在这里只是为了顺便记一下:

cat << EOF | sudo tee /etc/ssh/sshd_config.d/10-no-passwords.conf
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF

sshd_config.d 里的序号规则是 小的覆盖大的

重启 sshd:

# Linux systemd
systemctl restart ssh.service
systemctl status ssh.service

# macOS launchd
launchctl kickstart -k -p system/com.openssh.sshd
launchctl print system/com.openssh.sshd

让 VNC 远程桌面只能通过 ssh 隧道连接(仅监听 127.0.0.1):

sudo defaults write /Library/Preferences/com.apple.RemoteManagement.plist VNCOnlyLocalConnections -bool yes

ssh -L 5900:127.0.0.1:5900 mac-mini

小尾巴

保不齐这些 NAS 系统后续更新会出什么幺蛾子,可以让 AI 搓一个脚本,放在 Docker 容器里 crontab 定时运行,然后给一个服务器列表,一旦发现任何服务器允许 PasswordAuthentication,就发送通知告警。

修改后的效果:

ssh -o PreferredAuthentications=password,keyboard-interactive prin@192.168.1.10 -p 2222
# prin@192.168.1.10: Permission denied (publickey).

ssh -o PreferredAuthentications=password,keyboard-interactive prin@192.168.2.10 -p 2222
# prin@192.168.2.10: Permission denied (publickey).