如何配置安全的 SSH 服务?(OpenSSH 安全必知必会)

Linux

通过几个额外的步骤来保证服务器的安全。

SSH 对服务器管理至关重要。这篇文章将引导你通过一些选项可用于强化OpenSSH。这些说明可能适用于其他版本的 Linux,但适用于 Ubuntu 16.04 LTS。

**警告: **干扰 SSH 的工作原理可能很危险。您可以非常轻松地将自己锁定在服务器外。小心。

OpenSSH 服务配置

Ubuntu 16.04 操作系统上 OpenSSH 服务的配置文件路径为 /etc/ssh/sshd_config ,要配置它,必须得使用系统管理员( root )权限或使用 sudo 命令临时获得的管理员权限。

备份配置文件

在编辑任何配置文件前先备份它,不失为一个好习惯。

cp /etc/ssh/sshd_config /etc/ssh/backup.sshd_config

编辑配置文件

我不是潮人,故我老老实实地使用 nano 编辑配置文件。

nano /etc/ssh/sshd_config

SSH 配置测试

修改配置之后,最好要测试一下配置有效,甚至都不需重载服务。可以使用下面的命令。

sshd -t

重载使新配置生效

当配置更改完成,重启 SSH 后台服务使新的配置生效。

sudo systemctl reload sshd

检查网络协议

最初实践对配置文件的定制非常简单,重要的是闭环检查而不是冒失地写配置。打开 /etc/ssh/sshd_config 检查以 Protocol 开头的语句,保证它设置为 2 而不是 1 ,目前默认是 2 。

Protocol 2

禁用 Root 用户连接

替换使用 root 连接,通过 SSH 连接进来的用户应当给予 sudo 许可权限。保证您在本机具备 sudo 权限,去禁用 root 用户连接许可。 译注:上面基本按英文原文直译过来,按照译者的理解这里解释一下,不当之处,欢迎指正。 1. 首先,理解 root 用户和 sudo 权限,root 用户是系统中的一个用户,他拥有最高权限,可为所欲为。当长期在 root 用户下操作时,一个不当操作会使系统覆水难收。故现代许多系统中是会禁用这个用户的,那么,对系统级的管理如何进行? 出现了 sudo 权限,它指系统中一个普通用户,若具备 sudo 权限,可以通过 sudo 语句临时提升这个用户到 root 权限,一般为20分钟。这样避免长期在 root 下操作的安全性问题。2. 回到这里,原文的意思是,我们应禁用 root 用户连接,因为连接后会出现1中所说的安全问题,应当是给予那些有 sudo 权限的用户连接。 在配置文件中找到下面行:

PermitRootLogin yes

修改设置为 no:

PermitRootLogin no

关闭空闲会话

空闲会话可能造成安全问题。记录一个连接用户不活动的时间是个好办法。ClientAliveInterval 的值记录的是服务器最后一次发送给客户端活动信息后未得到客户响应持续到现在的时间秒数。下面例子是设置为每当客户在5分钟不响应,服务器发送询问,这样2次后将断开此会话。

ClientAliveInterval 300
ClientAliveCountMax 2

用户白板

可以配置哪些用户允许通过 SSH 登录。这就是白板列表。仅白板列表里的用户可连接登录。白板之外的用户将被禁止  。来看下如何设置允许 norton 用户通过 SSH 远程登录。 加入以下语句:

AllowUsers norton

您想让谁 SSH 登录,别忘了加其用户名到 AllowUser(允许用户) 列表里。

改变网络端口

我另外极少用到的加固 SSH 的策略是改变 SSH 默认监听网络端口。通常,默认的 SSH 网络监听端口号为22,您可以在您的配置中改变您的 SSH 服务监听端口号。主要基于网络中充斥着一帮拿着现成的脚本搜索网络漏洞机会的弱智家伙,这些现成的脚本,只会对那些知名端口扫描。若您改变了您的默认 SSH 服务端口 22 到其它,会极大减少这些无聊的骚扰。我不会这么做也不鼓励这么做。但是,您可能需要。 配置文件中找到如下行:

Port 22

将它的设置改成其它什么的数值,如 2222( >1024,< 65535 )。

Port 2222

SSH 密钥

通过 SSH 登录系统默认使用用户名和密码组合。这种方式存在被暴力破解的可能。攻击者会用海量的用户名/密码组合(译注:也可能是字典方式)不断尝试找到能进入的组合。因此,应当使用 SSH 密钥登录方式替掉用户名/密码方式。

生成密钥对

若已有密钥对(公钥,私钥),跳过这里。

将生成公开加密密钥对,它成对生成。一个私钥,一个公钥。

client machine(客户机) 运行以下命令。不要使用 sudo 权限,因为密钥是通过密码口令保护的(暗指跟用户权限无关)。当然,你可以不指定保护口令,使它为空,但是,我不主张这样。因为没有口令保护的密钥文件,被别有用心的人得到的话,他可以畅行无阻地通过 SSH 登录到您的机器。(译注:这里的 client machine 是指欲使之连接到运行 SSH 服务的服务器的客户机)

ssh-keygen

上传您的公钥

使用 ssh-copy-id 命令去上传您客户机的公钥到 SSH 服务器上。

ssh-copy-id jason@192.168.1.1

现在进行 SSH 登录,可能会被提示要求 SSH 口令 (若设置)

ssh jason@192.168.1.1

您可能会看到类似如下屏幕显示:

The authenticity of host '192.168.1.1 (192.168.1.1)' can't be established.
ECDSA key fingerprint is ff:fd:d5:f9:66:fe:73:84:e1:56:cf:d6:ff:ff.
Are you sure you want to continue connecting (yes/no)?

输入 yes 后您会无需用户密码远程登录进运行着 SSH 服务的机器。

禁止用户密码认证登录

SSH 密钥认证方式有效之后,应将用户密码认证登录禁用掉。配置文件中找到:

PasswordAuthentication yes

改变其值为 no 。

PasswordAuthentication no

禁止 X11 传输

本指南旨在指导对远程服务器的连接配置。一般,服务器上是不装 GUI 的。因此,应禁用 X11 方式远程连接登录。配置文件中找到:

X11Forwarding yes

修改其值为 no 。

X11Forwarding no

使用 Fail2Ban 软件防御洪泛

Fail2Ban 是一款极好的软件防御洪泛(译注:Fail2Ban 字面意思从失败到禁止。前面提到网络上充斥着各种的网络攻击,有一种攻击是拒绝服务攻击--使用大量的请求导致服务端口资源耗尽而停止正常服务。对于 SSH 同样存在,网络上的恶意试探您 SSH 服务的行为,当大量的肉鸡被驱动来试探您的 SSH 服务,它们的每次请求,服务都要响应。当这样的行为成为海量。服务将会超负荷而不能对正常服务响应。这里个人翻译为洪泛,不当之处请指正)。这个软件通过扫描 SSH 服务的日志来及时发现恶意连接请求,将其请求的来源 IP 地址暂时禁用(在网络路由中禁用,使服务不需响应其请求而防御拒绝服务攻击)必须安装这个 Fail2ban 软件才能使用这个能力。

apt-get install fail2ban

安装完成后,复制 fail2ban 配置文件,/etc/fail2ban/jail.conf --> /etc/fail2ban/jail.local

cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

打开 /etc/fail2ban/jail.local 文件找到 [sshd] 段,修改成下面所示,并加入  enabled = true 语句:

[sshd]enabled  = true
port    = ssh
logpath = %(sshd_log)s

重启 fail2ban

service fail2ban restart

Fail2ban 会自动监控 SSH 日志发现可疑的恶意行为,暂时禁止其来源 IP 访问本机。

多重技术手段保护认证过程

也可使用 TOTP (基于时间戳的一次性动态口令)来加固 SSH 的安全。下面的例子,用到了谷歌认证器 --  Google Authenticator --。 (译注:简单解释一下 TOTP 原理。服务端与客户端在同等初始条件下,每隔固定时间间隔产生一串随机码。当服务和客户端在相同初始条件,时间相同,算法相同的情况下,能保证各自产生的码是一致的,这个就可以验证彼此。译者水平有限,解释不当之处,敬请指正)当试图登录到服务器时,会被要求一个验证码。我们可以使用 Google Authenticator app 读到这个验证码。首先,安装软件。

sudo apt-get install libpam-google-authenticator

然后初始化

google-authenticator

会被询问: Do you want authentication tokens to be time-based (y/n) 输入 yes. 屏幕输出二维码(app 用)并询问是否更新谷歌认证器(Google Authenticator ) 文件 --  .google_authenticator -- 。

Linux

不要担心这项服务不再提供或这些码没用。

使用 谷歌认证器(Google Authenticator)app 扫描上面的二维码,并且好好保管那几个 应急码(emergency codes) 接下来您会被询问一些问题,我们这里全部回答 yes 。

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases your chances to notice or even prevent man-in-the-middle attacks (y/n) yBy default, tokens are good for 30 seconds and in order to compensate for possible time-skew between the client and the server, we allow an extra token before and after the current time. If you experience problems with poor time synchronization, you can increase the window from its default size of 1:30min to about 4min. Do you want to do so (y/n) yIf the computer that you are logging into isn't hardened against brute-force login attempts, you can enable rate-limiting for the authentication module. By default, this limits attackers to no more than 3 login attempts every 30s. Do you want to enable rate-limiting (y/n) y)

编辑服务器上的 PAM 管理文件 --  /etc/pam.d/sshd -- 文件尾添加如下指令:

auth required pam_google_authenticator.so

编辑 ssh 配置文件 /etc/ssh/sshd_config 如下:

UsePAM yes

ChallengeResponseAuthentication yes

重启 SSH 服务。这样,每次登录系统时,将会要求输入一个验证码。
(译注:这节内容,限于我们的网络环境,您可能无法实验。自行查找资料学习思考,这里就不科普了。)

门匾设置

人们常常讨论门匾(译注:banner 英语中本意为横幅,这里指登录系统时显示给客户屏幕上的欢迎词或对系统的自我介绍。这里译者翻译为门匾,纯个人水平所限,希望诸位指正,找到更好的简洁翻译)会泄露您系统一些内情,给攻击者一些可攻击的信息。因此,在 Ubuntu 16.04 系统下 SSH 服务配置默认禁止显示信息给客户。让我们打开这个设置看看发生什么。banner(门匾) 信息,无论客户成功登录与否都会先显示给客户。每个试图 SSH 连接登录到服务器都会看到 SSH 的 banner 。也许需要打开 PasswordAuthentication 配置才能看到 banner 。

编辑 SSH 配置文件,找到如下指令,并将其开头的注释符去掉:

#Banner /etc/issue.net

现在,尝试使用猜测的用户名去登录 SSH 服务器:

ssh fake_user@192.168.1.1

将看到返回的屏幕信息。

Ubuntu 16.04.3 LTS
fake_user@192.168.1.1’s password:

现在您看到您的操作系统信息已泄露给那个尝试的未知者了。

可以编辑 /etc/issue.net文件改变 banner 的内容。这里加入了一点点 ascii art bunny 欢迎我们的「客户」。

______________________
|                    |
| Welcome Leet Haxor |
|____________________|
       ||
(\_/)  ||
( *,*) ||
(")_(")

现在,尝试使用猜测的用户名去登录 SSH 服务器:

ssh fake_user@192.168.1.1

得到 banner 欢迎信息:

______________________
|                    |
| Welcome Leet Haxor |
|____________________|
       ||
(\_/)  ||
( *,*) ||
(")_(")fake_user@192.168.1.1's password:

现在,那些想给我们系统捣乱的家伙得好好琢磨琢磨了。

SSH 安全性审核

到这儿,已基本讲解完 SSH 基础配置。现在将进阶到高级 SSH 安全加固课题。SSH Audit 是扫描 SSH 服务安全隐患的Python 脚本。依上面链接下载并执行,命令行中指向需测试的 SSH 服务器地址。

python ssh-audit.py labs.seattlebot.net

将会得到一个庞大的测试报告。

Linux

这个报告将揭开蒙在 SSH 服务安全性上的面纱。这是关于您的 SSH 服务器与客户通讯使用的密文加密算法的安全性报告。若曾经学过或运行过如 OpenSSL 加密协议,会看到过类似的报告。不是所有的密文加密算法都同等坚固。不同的方面,或强、或弱。正确评估这些算法的强弱有助于坚固 SSH 服务系统的安全性。

修改备选的主机密钥

按照 stribika, mozilla 文档中的建议和我们自己的 SSH 安全报告,在 SSH 服务配置文件中移除其它的备选主机密钥,只保留如下2个。

HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key

修改默认使用的密文加密算法

继续按照 stribikamozilla 文档中的建议和自己的 SSH 安全审核报告,修改我们的密钥加密交换,对称加密密文, 信息验证码等加密算法。在 SSH 服务配置文件中,添加或修改成如下指令语句。

KexAlgorithms curve25519-sha256@libssh.orgCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctrMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com

再次 SSH 安全审核

看看配置的改变是否使安全审核满意。

python ssh-audit.py 173.255.250.98

Linux

绿色表示满意。

这样看起来好多了。

重建 Moduli

/etc/ssh/moduli 文件中保存着 SSH 用于 Diffie-Hellman key exchange(迪菲-赫尔曼密钥交换) 质数发生器码。您当前的 /etc/ssh/moduli 可能已不能保证唯一性。重新生成它可以坚固 SSH 服务的安全性。生成它,可能得需要点儿时间。

ssh-keygen -G moduli-2048.candidates -b 2048
ssh-keygen -T moduli-2048 -f moduli-2048.candidates
cp moduli-2048 /etc/ssh/moduli
rm moduli-2048

结语

但愿拙文能给您有所启发或帮助您的 SSH 服务器坚固而难以攻破。安全领域中的 OpenSSH,有太多的东西需要我们不断学习。保重。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@jasonrigden/hardenin...

译文地址:https://learnku.com/server/t/36120

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 3

本来,两天前计划完成此篇翻译。无奈周休有些活动缠身。好在按自己的计划,今晚完成了。自觉不太成样子,在此啰嗦几句

  1. 非常感谢大牛们的宽容,审阅译文通过。
  2. 安全协议,加密算法本就是广阔,坚深的课题。原文作者也是抛砖引玉之意。本人更是班门弄斧。
  3. 文中涉及到的一些知识点,尤其是我不熟悉的都经过查资料,也只是粗知一二,尽可能地进行实验。
  4. 综上,我画蛇添足地添加了许多译注。必然有许多不当和错误之处。敬请大牛们指正,一定诚心改过。

注:本文中的配置,本人亲试在ubuntu 16.04下有效。 ubuntu 18.04下配置似乎迥然不同(也许OpenSSH版本不同)。

-- 感谢信息技术领域先贤大哲们的无私奉献 --

4年前 评论
Summer

@acHao 翻译得很棒,备注也很贴心。

4年前 评论

TOPT也可以用PHP来计算出来

<?php
/*
    TOTP v0.2.1 - a simple TOTP (RFC 6238) class using the SHA1 default

    (c) 2014 Robin Leffmann <djinn at stolendata dot net>

    https://github.com/stolendata/totp/

    Licensed under CC BY-NC-SA 4.0 - http://creativecommons.org/licenses/by-nc-sa/4.0/
*/

class TOTP
{
    private static $base32Map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

    private static function base32Decode( $in )
    {
        $l = strlen( $in );
        $n = $bs = 0;

        for( $i = 0; $i < $l; $i++ )
        {
            $n <<= 5;
            $n += stripos( self::$base32Map, $in[$i] );
            $bs = ( $bs + 5 ) % 8;
            @$out .= $bs < 5 ? chr( ($n & (255 << $bs)) >> $bs ) : null;
        }

        return $out;
    }

    public static function getOTP( $secret, $digits = 6, $period = 30, $offset = 0 )
    {
        if( strlen($secret) < 16 || strlen($secret) % 8 != 0 )
            return [ 'err'=>'length of secret must be a multiple of 8, and at least 16 characters' ];
        if( preg_match('/[^a-z2-7]/i', $secret) === 1 )
            return [ 'err'=>'secret contains non-base32 characters' ];
        $digits = intval( $digits );
        if( $digits < 6 || $digits > 8 )
            return [ 'err'=>'digits must be 6, 7 or 8' ];

        $seed = self::base32Decode( $secret );
        $time = str_pad( pack('N', intval($offset + time() / $period)), 8, "\x00", STR_PAD_LEFT );
        $hash = hash_hmac( 'sha1', $time, $seed, false );
        $otp = ( hexdec(substr($hash, hexdec($hash[39]) * 2, 8)) & 0x7fffffff ) % pow( 10, $digits );

        return [ 'otp'=>sprintf("%'0{$digits}u", $otp) ];
    }

    public static function genSecret( $length = 24 )
    {
        if( $length < 16 || $length % 8 !== 0 )
            return [ 'err'=>'length must be a multiple of 8, and at least 16' ];

        while( $length-- )
        {
            $c = @gettimeofday()['usec'] % 53;
            while( $c-- )
                mt_rand();
            @$secret .= self::$base32Map[mt_rand(0, 31)];
        }

        return [ 'secret'=>$secret ];
    }

    public static function genURI( $account, $secret, $digits = null, $period = null, $issuer = null )
    {
        if( empty($account) || empty($secret) )
            return [ 'err'=>'you must provide at least an account and a secret' ];
        if( mb_strpos($account . $issuer, ':') !== false )
            return [ 'err'=>'neither account nor issuer can contain a colon (:) character' ];

        $account = rawurlencode( $account );
        $issuer = rawurlencode( $issuer );
        $label = empty( $issuer ) ? $account : "$issuer:$account";

        return [ 'uri'=>'otpauth://totp/' . $label . "?secret=$secret" .
                        (is_null($digits) ? '' : "&digits=$digits") .
                        (is_null($period) ? '' : "&period=$period") .
                        (empty($issuer) ? '' : "&issuer=$issuer") ];
    }
}


echo TOTP::getOTP('xxx')['otp'];
4个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!