天行健,君子以自强不息;地势坤,君子以厚德载物;

基于LDAP统一账户认证+动态口令的VPN架构实现

架构简图

《基于LDAP统一账户认证+动态口令的VPN架构实现》

前言

随着云服务的发展、成熟,越来越多的企业或个人使用云服务,在降低采购成本的同时,还可以降低运维、部署成本。鉴于IPV4地址的有限性,越来越多的云企业采用私有网络(VPC)模式做为网络服务首选,甚至有的云企业只允许使用VPC网络模式。从安全层面来说,将内部服务放置在VPC内,也可以有效提高整体的安全性。在VPC网络模式下,VPN的使用成为必须需求。

本文简化了相关服务的部署、配置环节,适用于具备一定技术基础人员阅读。

OpenLDAP部署

版本:openldap-2.4.48

安装部署略……

OpenLDAP,实现账号的统一管理,部署、配置文章网上很多,就不多述了,大家自行参考。做为底层基础服务,在架构设计上建议做成多主或多主多从模式。

OpenLDAP的配置并不算复杂,核心点是服务鉴权的识别。默认策略情况下,用户权限正确,就允许用户登录所有系统。例如,有A、B、C、D等服务使用了OpenLDAP做账户管理,用户X加入OpenLDAP后,就可以同时登录A、B、C、D等服务,在安全上显然是不合适的。如何管控用户能登录哪些服务系统,是OpenLDAP配置的关键。当然方法是多种的,目前我采用的方式是ACL+关键字识别方式来授权用户可以登录的服务。

multiOTP部署

版本:multiotp-5.6.1.5

存放目录:/data/multiotp

multiOTP是一款开源的二次认证PHP类,已通过OATH认证,适用于HOTP / TOTP。

官网:http://www.multiOTP.net/

下载解压即可,multiOTP需要和freeRadius部署在相同的设备上

参考官方资料做相关参数配置


# ---------
# constants
# ---------
MYNAM=`basename "$0"`
MYDIR=`adirname "$0"`
MOTP="${MYDIR}/multiotp.php"

# ----
# main
# ----

# an account is temporary locked for 300 seconds after 3 unsuccessful trials.
${MOTP} -config failure-delayed-time=60
${MOTP} -config max-delayed-failures=60
# After XX (default:6) unsuccessful trials, the account is definitely locked.
${MOTP} -config max-block-failures=100
${MOTP} -config server-cache-level=0
${MOTP} -config issuer="QWQ"

# created users need to type a prefix PIN (1|0)
${MOTP} -config default-request-prefix-pin=1
# created users need to type their LDAP password instead of PIN
${MOTP} -config default-request-ldap-pwd=1
# Set the AD/LDAP server type (1=Active Directory | 2=standard LDAP)
${MOTP} -config ldap-server-type=2
# Set the user CN identifier (uid for standard LDAP)
${MOTP} -config ldap-cn-identifier="uid"
# Set the group CN identifier (cn for standard LDAP)
${MOTP} -config ldap-group-cn-identifier="cn"
# Set the group attribute
${MOTP} -config ldap-group-attribute="uniqueMember"
# SSL connection or not (0|1)
${MOTP} -config ldap-ssl=0
# Set the default port (389=regular | 636=SSL connection)
${MOTP} -config ldap-port=389
# Set the LDAP server(s), comma separated, you can define more than one server, and you can also use a SSL connection only for one server, on a specific port
${MOTP} -config ldap-domain-controllers=ldap://ldap.quwenqing.com:389
# Base DN
${MOTP} -config ldap-base-dn="dc=quwenqing,dc=com"
# Bind DN (which is the account used to connect to the AD/LDAP)
${MOTP} -config ldap-bind-dn="uid=vpn,ou=Admins,dc=quwenqing,dc=com"
# Set the password of the user used to search in the LDAP directory
${MOTP} -config ldap-server-password="{Your Password}"
# LDAP/AD users DN (optional, use base-dn if empty)
${MOTP} -config ldap-users-dn="ou=People,dc=quwenqing,dc=com"
# In which groups users must be in LDAP directory in order to be added
${MOTP} -config ldap-in-group="developers"
# Set the network timeout
${MOTP} -config ldap-network-timeout=10
# Set the transaction time limit
${MOTP} -config ldap-time-limit=30
# default algorithm for new LDAP/AD users [totp|hotp|motp|without2fa]
${MOTP} -config ldap-default-algorithm=totp
# Activate the AD/LDAP support (0|1)
${MOTP} -config ldap-activated=1
${MOTP} -config ldap-cache-on=0

做好上述配置后,执行如下操作,拉取LDAP相关信息生成本地用户信息
${MOTP} -debug -display-log -ldap-check
${MOTP} -debug -display-log -ldap-users-sync

原脚本在执行LDAP用户鉴权有异常,请参照如下修改:

--- multiotp-5.6.1.5-source/multiotp.php        2019-10-24 04:38:28.000000000 +0800
+++ multiotp-5.6.1.5/multiotp.php       2020-01-03 15:57:02.236797020 +0800
@@ -1,4 +1,5 @@
-#!/usr/bin/php
+#!/usr/local/php/bin/php -dextension=ldap.so
<?php
 /* multiOTP command line version (all-in-one) */
 /**
@@ -14135,10 +14136,13 @@
 
       // DistinguishedName must be encoded in UTF-8
       $ldap_bind_dn = encode_utf8_if_needed($ldap_username);
-      
-      if (('' != $ldap_username) && (FALSE === mb_strpos(mb_strtolower($ldap_bind_dn), 'cn='))) {
-          $ldap_bind_dn = 'CN='.$ldap_bind_dn.','.$this->GetLdapBaseDn();
+     
+      // Changed by QuWenQing
+      $ldap_cn_identifier = $this->GetLdapCnIdentifier();
+      if (('' != $ldap_username) && (FALSE === mb_strpos(mb_strtolower($ldap_bind_dn), mb_strtolower($ldap_cn_identifier.'=')))) {
+          $ldap_bind_dn = $ldap_cn_identifier.'='.$ldap_bind_dn.','.$this->GetLdapUsersDn();
       }
+      //$this->WriteLog("User: ".$ldap_bind_dn."  PWD: ".$ldap_password);
 
       if (!function_exists('ldap_connect')) {
           $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, 39, 'System', '', 3);
@@ -23594,14 +23598,14 @@
                     $this->_bind = @ldap_bind($this->_conn,$this->_ad_username.$this->_account_suffix,$this->_ad_password);
                     $this->_bind_paged = @ldap_bind($this->_conn_paged,$this->_ad_username.$this->_account_suffix,$this->_ad_password);
                     if ($this->_bind) {
-                        if (FALSE !== (@ldap_search($this->_conn, $this->_base_dn, "(dn=test-connection)"))) {
+                        if (FALSE !== (@ldap_search($this->_conn, $this->_ad_username.$this->_account_suffix, "(cn=*)"))) {
                             $this->_error = FALSE;
                             $this->_error_message = '';
                             $connected = TRUE;
                             break;
                         } else {
                             $this->_error = TRUE;
-                            $this->_error_message = 'FATAL: AD/LDAP bind failed. The BaseDN '.$this->_base_dn.' is not accepted.';
+                            $this->_error_message = 'FATAL: AD/LDAP bind failed. The DN '.$this->_ad_username.$this->_account_suffix.' is not accepted.';
                         }
                     } else {
                         $this->_server_reachable = (!(-1 == ldap_errno($this->_conn)));

请确认调用的php.ini包含ldap.so模块,或如上述,在脚本运行时动态调用。

然后就可以执行如下命令来校验LDAP用户

/data/multiotp/multiotp.php -debug -check-ldap-password ${user} ${password}

验证成功,使用如下命令来生成用户二维码 (only for TOTP and HOTP):

/data/multiotp/multiotp.php -debug -qrcode quwenqing quwenqing.png

freeRadius3部署

版本:freeradius-server-3.0.20

安装部署略……

参考文档:https://wiki.freeradius.org/guide/multiOTP-HOWTO#freeradius-3-x-x

1、建立raddb/mods-available/multiotp

exec multiotp {
        wait = yes
        input_pairs = request
        output_pairs = reply
        program = "/data/multiotp/multiotp.php %{User-Name} %{User-Password} -request-nt-key -src=%{Packet-Src-IP-Address} -chap-challenge=%{CHAP-Challenge} -chap-password=%{CHAP-Password} -ms-chap-challenge=%{MS-CHAP-Challenge} -ms-chap-response=%{MS-CHAP-Response} -ms-chap2-response=%{MS-CHAP2-Response} -debug"
        shell_escape = yes
        #packet_type = Access-Accept
}
将raddb/mods-available/multiotp软链到 raddb/mods-enabled/multiotp

2、复制 raddb/mods-available/mschap 为 raddb/mods-available/multiotpmschap ,做如下修改

"mschap {"
to
"mschap multiotpmschap {"

ntlm_auth = "/data/multiotp/multiotp.php %{User-Name} %{User-Password} -request-nt-key -src=%{Packet-Src-IP-Address} -chap-challenge=%{CHAP-Challenge} -chap-password=%{CHAP-Password} -ms-chap-challenge=%{MS-CHAP-Challenge} -ms-chap-response=%{MS-CHAP-Response} -ms-chap2-response=%{MS-CHAP2-Response}"
将 raddb/mods-available/multiotpmschap 软链到 raddb/mods-enabled/multiotpmschap

3、建立 raddb/policy.d/multiotp

# Change to a specific prefix if you want to deal with normal PAP authentication as well as OTP
# e.g. "multiotp_prefix = 'otp:'"
multiotp_prefix = ''
multiotp.authorize {
    # This test force multiOTP for any MS-CHAP(v2) attempt
    if (control:Auth-Type == MS-CHAP) {
        update control {
            Auth-Type := multiotpmschap
        }
    }
    # This test force multiOTP for any MS-CHAP(v2) attempt
    elsif (control:Auth-Type == mschap) {
        update control {
            Auth-Type := multiotpmschap
        }
    }
    # This test force multiOTP for any CHAP attempt
    elsif (control:Auth-Type == chap) {
        update control {
            Auth-Type := multiotp
        }
    }
    # This test is for decimal OTP code only, otherwise you will have to change it
    #  elsif (!control:Auth-Type && User-Password =~ /^${policy.multiotp_prefix}([0-9]{10})$/) {
    #
    # Use this simple test for non decimal only OTP code: elsif (!control:Auth-Type) {
    #
    # This test force multiOTP for any other attempt like PAP
    elsif (!control:Auth-Type) {
        update control {
            Auth-Type := multiotp
        }
    }
}

4、建立 raddb/sites-available/multiotp

server multiotp { 
        listen { 
                ipaddr = 127.0.0.1
                port = 11111
                type = auth
                #proto = tcp
        }
        authorize {
                # Handle multiotp authentication
                multiotp
        }
        authenticate {
                Auth-Type multiotp {
                        multiotp
                }
                Auth-Type multiotpmschap {
                        multiotpmschap
                }
        }
}
将 raddb/sites-available/multiotp 软链到 raddb/sites-enabled/multiotp

5、修改 raddb/proxy.conf ,将 realm NULL 调整如下:

realm NULL {
        authhost        = 127.0.0.1:11111
        secret          = {Your Secret}
}

OCServ部署

与OpenVPN二选一或者都部署

版本:ocserv-0.12.6

安装部署略……

yum install radcli

修改配置文件中的认证方式为:

auth = "radius[config=/etc/radcli/radiusclient.conf,groupconfig=true,nas-identifier=VPN]"

修改 /etc/radcli/radiusclient.conf ,设置freeRadius Server相关信息

修改 /etc/radcli/servers ,设置 freeRadius Secret

OpenVPN部署

与OCServ二选一或者都部署

版本:openvpn-2.4.8

安装部署略……

安装OpenVPN的freeRadius模块

wget http://www.nongnu.org/radiusplugin/radiusplugin_v2.1a_beta1.tar.gz

解包并编译

cd radiusplugin_v2.1a_beta1/

make

完成后会生成一个名为radiusplugin.so的文件,将该文件及.cnf的文件移动到OpenVPN相关的目录:

cp radiusplugin.cnf /etc/openvpn/

cp radiusplugin.so /usr/lib64/openvpn/plugin/lib/radiusplugin.so

修改 radiusplugin.cnf 相关参数

关键点: 将 server 节中的wait时间设置长些,例如 wait=30

修改OpenVPN的配置文件,添加如下配置:

client-cert-not-required

username-as-common-name

reneg-sec 0

plugin /usr/lib64/openvpn/plugin/lib/radiusplugin.so /etc/openvpn/radiusplugin.cnf

修改OpenVPN服务端和客户端配置,增加如下配置选项,解决每小时断线的问题

reneg-sec 0

https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/

--reneg-sec n

Renegotiate data channel key after n seconds (default=3600).

When using dual-factor authentication, note that this default value may cause the end user to be challenged to reauthorize once per hour.

Also, keep in mind that this option can be used on both the client and server, and whichever uses the lower value will be the one to trigger the renegotiation. A common mistake is to set --reneg-sec to a higher value on either the client or server, while the other side of the connection is still using the default value of 3600 seconds, meaning that the renegotiation will still occur once per 3600 seconds. The solution is to increase --reneg-sec on both the client and server, or set it to 0 on one side of the connection (to disable), and to your chosen value on the other side.

OpenVPN中 reneg-sec 参数定义了一段时间之后(缺省:3600秒)需要重新验证key,这个参数在服务端和客户端设置都有效 如果两边都设置了,那么就按照时间短的设定优先。当两边同时设置成0,表示禁用TSL重协商。

Google身份认证

安装Google身份认证APP,基本各大应用市场都有下载,对应手机系统下载对应的Android 或IOS版本

打开应用,添加-扫描条形码,扫描multiOTP生成的用户二维码。

VPN Client

根据VPN服务端部署选择相应客户端应用

OCServ:

安装CISCO AnyConnect

OpenVPN:

Windows:Openvpn

MacOS:Tunnelblick

密码部分,直接输入LDAP用户密码和动态密码,例如LDAP密码为abcde,动态密码为123456,则密码输入  abcde123456

后记

前述部署只是介绍了主体思路及其中的几个关键点,若适合业务应用,需要细化的方面还很多,例如 multiOTP 后端可以设置为MySQL,这样就可以多台部署。结合管理后台,还可以实现用户动态密码的自签发、销毁、重签等行为。

参考文献

OpenLDAP:http://www.openldap.org/doc/admin24/

multiOTP:https://github.com/multiOTP/multiotp/wiki

freeRADIUS:https://wiki.freeradius.org/guide/multiOTP-HOWTO#freeradius-3-x-x

OCServ:http://ocserv.gitlab.io/www/manual.html

OpenVPN:https://openvpn.net/community-resources/#documentation

点赞

发表评论