Tommy 碎碎念

Tommy Wu's blog

« 上一篇 | 下一篇 »

架設 OpenConnect VPN server
post by tommy @ 15 七月, 2016 10:05

在 Debian 中安裝 OpenConnect VPN server 其實很簡單, 應該只要一行 apt-get install ocserv 指令就可以了.

安裝後只要去修改 /etc/ocserv/ocserv.conf 設定, 改變 tcp-port, udp-port, server-cert, server-key 就可以:

# TCP and UDP port number
# When systemd is running as init, this needs to be adjusted in
# the corresponding unit file as well.
tcp-port = 8443
udp-port = 8443
 
# The key and the certificates of the server
# The key may be a file, or any URL supported by GnuTLS (e.g.,
# tpmkey:uuid=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx;storage=user
# or pkcs11:object=my-vpn-key;object-type=private)
#
# The server-cert file may contain a single certificate, or
# a sorted certificate chain.
#
# There may be multiple server-cert and server-key directives,
# but each key should correspond to the preceding certificate.
server-cert = /etc/letsencrypt.sh/certs/default/fullchain.pem
server-key = /etc/letsencrypt.sh/certs/default/privkey.pem

預設的 port 是 443, 不過... 通常 443 都給 https 用了, 所以就找別的 port 來用... 如果你的系統是用 systemd (如果不是很久沒更新的版本, 應該都是了), 還用去改 /lib/systemd/system/ocserv.socket 裡頭的設定才可以.

憑證的部份可以用 Let's encrypt 的憑證就可以.

由於 OpenConnect 與 Cisco AnyConnect 基本上是相容的, 只要加上下面的設定就可以:

# This option must be set to true to support legacy CISCO clients.
# A side effect of this option is that it will no longer be required
# for clients to present their certificate on every connection.
# That is they may resume a cookie without presenting a certificate
# (when certificate authentication is used).
cisco-client-compat = true
 
# Client profile xml. A sample file exists in doc/profile.xml.
# It is required by some of the CISCO clients.
# This file must be accessible from inside the worker's chroot.
# Note that enabling this option is not recommended as it will allow
# the worker processes to open arbitrary files (when isolate-workers is
# set to true).
user-profile = /etc/ocserv/profile.xml

user-profile 並非一定要 (有設定這個時, 在 AnyConnect Client 上頭會用名稱取代 ip adreess 或 hostname), profile.xml 的內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<AnyConnectProfile xmlns="http://schemas.xmlsoap.org/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.xmlsoap.org/encoding/ AnyConnectProfile.xsd">
 
<ClientInitialization>
<UseStartBeforeLogon UserControllable="false">false</UseStartBeforeLogon>
<StrictCertificateTrust>false</StrictCertificateTrust>
<RestrictPreferenceCaching>false</RestrictPreferenceCaching>
<RestrictTunnelProtocols>IPSec</RestrictTunnelProtocols>
<BypassDownloader>true</BypassDownloader>
<WindowsVPNEstablishment>AllowRemoteUsers</WindowsVPNEstablishment>
<CertEnrollmentPin>pinAllowed</CertEnrollmentPin>
<CertificateMatch>
<KeyUsage>
<MatchKey>Digital_Signature</MatchKey>
</KeyUsage>
<ExtendedKeyUsage>
<ExtendedMatchKey>ClientAuth</ExtendedMatchKey>
</ExtendedKeyUsage>
</CertificateMatch>
 
<BackupServerList>
<HostAddress>vpn.teatime.com.tw</HostAddress>
</BackupServerList>
</ClientInitialization>
 
<ServerList>
<HostEntry>
<HostName>TeaTime VPN Server</HostName>
<HostAddress>vpn.teatime.com.tw</HostAddress>
</HostEntry>
</ServerList>
</AnyConnectProfile>

就是改 HostName 與 HostAddress 就可以.

至於 client 端的程式, 可以用 OpenConnect 或 Cisco AnyConnect 的都可以 (各平台幾乎都有), 不過建議用 OpenConnect 會比較好用... Cisco 的 AnyConnect 似乎只會保留最後一次連線的設定 (是我不會用嗎?), 如果要連不同的 server, 不能簡單的選擇 profile 來用.

最後, 通常是直接用 pam 來管理可以連線的使用者 (可以搭配 pam_listfile.so 來控管), 如果要搭配 AD 帳號來用的話, 官網有篇文章有提到, 不過... 看起來似乎很麻煩... 我會選擇用 pam_script.so 來處理.

/etc/pam.d/ocserv:

#
# /etc/pam.d/ocserv - specify the PAM behaviour of ocserv
#
 
#auth required pam_listfile.so item=user sense=allow file=/etc/users/vpn onerr=fail
 
# Standard Un*x authentication.
auth required pam_script.so
 
# Disallow non-root logins when /etc/nologin exists.
account required pam_nologin.so
 
# Uncomment and edit /etc/security/access.conf if you need to set complex
# access limits.
# account required pam_access.so
 
# Standard Un*x authorization.
account required pam_script.so
 
# SELinux needs to be the first session rule. This ensures that any
# lingering context has been cleared. Without out this it is possible
# that a module could execute code in the wrong domain.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
 
# Standard Un*x session setup and teardown.
session required pam_script.so
 
# Set up user limits from /etc/security/limits.conf.
session required pam_limits.so
 
# SELinux needs to intervene at login time to ensure that the process starts
# in the proper default security context. Only sessions which are intended
# to run in the user's context should be run after this.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
 
password required pam_script.so

在 Debian 底下, pam_script.so 的指令放 /usr/share/libpam-script 目錄下. 需要有 pam_script_acct, pam_script_auth, pam_script_passwd, pam_script_ses_open, pam_script_ses_close 這幾個檔案 (用不到也要用, 不然 pam 會失敗).

我們只會用到 pam_script_auth 這一個, 其他的, 可以直接用:

#!/bin/sh
 
exit 0

表示成功就可以. 而 pam_script_auth 這一個, 我們可以使用 PHP 的 ldap 去做 AD bind 的動作並查詢是否屬於可以使用 VPN 的 group, 就可以透過 AD 來控管可連線的使用者了.

例如:

#!/usr/bin/php -Cq
<?php
 
$root_path = dirname($argv[0]);
if ($root_path !== '')
$root_path .= '/';
require_once($root_path.'config.php');
 
$aGroup = array();
 
function writelog($str)
{
global $root_path;
 
echo $str."\n";
$fp = fopen($root_path.'log/ocserv_auth_'.strftime('%Y%m%d').'.log', 'at');
if ($fp) {
fputs($fp, strftime('%H:%M:%S')." $str\n");
fclose($fp);
}
return;
}
 
function getAllGroupForGroup($ldap, $group_dn)
{
global $base_dn;
global $aGroup;
 
$sr = ldap_search($ldap, $base_dn, '(distinguishedName='.$group_dn.')');
$data = ldap_get_entries($ldap, $sr);
if ($data['count'] <= 0)
return false;
if (!array_key_exists('memberof', $data[0]))
return false;
$cnt = $data[0]['memberof']['count'];
for ($i = 0; $i < $cnt; $i++) {
$group = $data[0]['memberof'][$i];
if (!in_array($group, $aGroup)) {
$aGroup[] = $group;
getAllGroupForGroup($ldap, $group);
}
}
return true;
}
 
function getAllGroupForUser($ldap, $empid)
{
global $base_dn;
global $sec_key;
global $aGroup;
 
$aGroup = array();
$sr = ldap_search($ldap, $base_dn, $sec_key.'='.$empid);
$data = ldap_get_entries($ldap, $sr);
if ($data['count'] <= 0)
return false;
if (!array_key_exists('memberof', $data[0]))
return false;
$cnt = $data[0]['memberof']['count'];
for ($i = 0; $i < $cnt; $i++) {
$group = $data[0]['memberof'][$i];
if (!in_array($group, $aGroup)) {
$aGroup[] = $group;
getAllGroupForGroup($ldap, $group);
}
}
return true;
}
 
/*
# PAM_SERVICE - the application that's invoking the PAM stack
# PAM_TYPE - the module-type (e.g. auth,account,session,password)
# PAM_USER - the user being authenticated into
# PAM_RUSER - the remote user, the user invoking the application
# PAM_RHOST - remote host
# PAM_TTY - the controlling tty
# PAM_AUTHTOK - password in readable text
*/

$emp_id = getenv('PAM_USER');
$ldap_user = $emp_id.'@'.$mydomain;
$ldap_pass = getenv('PAM_AUTHTOK');
$rhost = getenv('PAM_RHOST');
 
writelog("user=$emp_id, rhost=$rhost");
// Attempting fix from http://www.php.net/manual/en/ref.ldap.php#77553
putenv('LDAPTLS_REQCERT=never');
 
$ldap = false;
foreach ($ldap_servers as $server) {
$ldap = @ldap_connect('ldap://'.$server);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$encrypted = ldap_start_tls($ldap);
if (!$encrypted) {
ldap_close($ldap);
$ldap = false;
continue;
}
}
if (@ldap_bind($ldap, $ldap_user, $ldap_pass) === false) {
ldap_close($ldap);
$ldap = false;
continue;
}
break;
}
 
if ($ldap === false) {
writelog("bind failed, user=$emp_id, rhost=$rhost");
exit(1);
}
 
getAllGroupForUser($ldap, $emp_id);
ldap_close($ldap);
$ldap = false;
 
$in_group = false;
foreach ($aGroup as $group) {
if (strcasecmp($group, $vpn_group) == 0) {
$in_group = true;
break;
}
}
 
if (!$in_group) {
writelog("failed, not in VPN group, user=$emp_id, rhost=$rhost");
exit(1);
}
writelog("authenticated, user=$emp_id, rhost=$rhost");
exit(0);

設定的 config.php 要有下面幾個資料:

<?php
 
$ldap_servers = array('192.168.0.1', '192.168.0.2');
$mydomain = 'domain.teatime.com.tw';
$base_dn = 'DC=domain,DC=teatime,DC=com,DC=tw';
$sec_key = 'samaccountname';
$vpn_group = 'CN=VPN,DC=domain,DC=teatime,DC=com,DC=tw';

透過這樣的處理, 我覺得比官網那個方式簡單一些.

Del.icio.us Furl HEMiDEMi Technorati MyShare
迴響
暱稱:
標題:
個人網頁:
電子郵件:
authimage

迴響

  

Bad Behavior 已經阻擋了 208 個過去 7 天試圖闖關的垃圾迴響與引用。
Power by LifeType. Template design by JamesHuang. Valid XHTML and CSS