Захотелось собрать VPN-комбайн который бы пользователей брал из БД, настраивал фаервол под этого пользователя и писал логи в БД.
OpenVPN на каждое событие (подключение, отключение клиента) может вызывать внешнюю программу. Этим и воспользуемся.
Я использую FreeBSD, но все ниже описанное будет работать на любом Linux, нужно лишь изменить пути. В качестве БД будет выступать Postgresql. Авторизация по сертификату и паролю.
Скрипт взаимодействия с внешними сервисами буду писать на Perl
Создаем БД:
Описание таблиц в БД:
users — из названия понятно что в ней будут пользователи.
столбцы:
groups — таблица с группами
log — таблица логов, в нее буду писать старт и конец сессии
stat — таблица онлайн пользователей
Создаем таблицы:
Создам пользователя с правами чтения для таблиц users и groups, и записью в log и stat
Заполнение таблиц:
Для примера, создам две группы, admins с полным доступом к внутренней сети и rdp с доступом к серверам по RDP.
Соответсвенно создам двух пользователей:
100101 — админ
100102 — с доступом к RDP
В результате, создались две группы с id 1 и 2. В соответствии с этими id будут созданы tables в фаерволе, к которым в свою очередь будут определены соответствующие разрешения.
Сертификаты
Если еще нет корневого сертификата, его нужно создать. У меня, сертификаты находятся в /root/ca
Сертификат для сервера подписанный CA сертификатом:
ovpn.key, ovpn.crt и ca.crt необходимо скопировать на OpenVPN сервер.
Для генерации пользовательских сертификатов я использую следующий скрипт, он упаковывает ключ и сертификат пользователя в запароленный p12 файл.
Создадим сертификаты для наших пользователей
на выходе получатся два файла /root/ca/p12/100101.p12 и /root/ca/p12/100102.p12 и пароли к ним, которые скрипт напечатает в консоли. Эти файлы нужно будет установить на пользовательских компьютерах (планшетах/телефонах) в защищенное хранилище. Как правило пользователям пароли от контейнеров не сообщаются, это исключает возможность пользователям передавать другим лицам свои ключи.
сразу же можно создать файл отозванных сертификатов crl
Его необходимо скопировать на OpenVPN сервер.
Настройка сервера OpenVPN
Итак, в четырех местах в конфиге вызывается внешний скрипт:
tls-verify параметр который позволяет проверять сертификат клиента на некоторое соответствие. В моем случае я проверяю что в CN записано тоже самое что клиент передает в Login, иначе говоря проверяю чтобы CN = Login. Это позволяет исключить возможность того, что пользователь имеющий один сертификат попытался бы зайти под другим пользователем введя его логин и пароль. Так же я проверяю чтобы в поле OU было значение VPN. Оба значения я добавляю при генерации сертификатов пользователей.
client-connect вызывается скрипт на этапе подключения пользователя. Тут я добавляю адрес пользователя в соответствующую таблицу фаервола, и делаю записи в таблицы log и stat.
client-disconnect вызывается скрипт на этапе отключения пользователя. Удаляю записи из фаервола, делаю записи в таблицы log и stat.
auth-user-pass-verify проверка переданого логина и пароля в БД.
Собственно, ниже сам скрипт
Фаервол
В файрволе созданы таблицы где номер таблицы = id группы из таблицы groups
осталось на пользовательском устройстве установить p12, скопировать ca.crt и такой конфиг
Напоследок, вкусняшка для тех кто пользуется дашбордом dashing. Скрипт нужно положить в dashboard/jobs
vpn_stat.rb
Скачать базу GeoIP:
и распаковать ее в dashboard/geoip/.
На дашборд добавить плитку:
Выглядет это будет так:

Вот и все. Осталось разве что написать нехитрую веб админку для самых ленивых.
Спасибо за внимание.
OpenVPN на каждое событие (подключение, отключение клиента) может вызывать внешнюю программу. Этим и воспользуемся.
Я использую FreeBSD, но все ниже описанное будет работать на любом Linux, нужно лишь изменить пути. В качестве БД будет выступать Postgresql. Авторизация по сертификату и паролю.
Скрипт взаимодействия с внешними сервисами буду писать на Perl
Создаем БД:
psql -Upgsql template1
ctreate database vpn;
\q
Описание таблиц в БД:
users — из названия понятно что в ней будут пользователи.
столбцы:
- id
- login — в моей системе логин цифровой, просто так удобно.
- name — ФИО пользователя.
- password — пароль в MD5.
- groups_id — id группы к которой принадлежит пользователь.
- active — статус пользователя, по нему определяется может или нет, пользователь подключаться к vpn.
- hwkey — у меня есть пользователи которые используют eToken, это поле определяет таких. В случае если у пользователя eToken, то проверка по паролю не производится.
groups — таблица с группами
- id
- groupname — название группы.
- active — статус группы, активна или нет.
log — таблица логов, в нее буду писать старт и конец сессии
- id
- date — дата
- users_id — id пользователя
- realaddress — реальный ip адрес
- virtualaddress — выданный OpenVPN сервером внутренний адрес
- action — сюда пишется событие( start/stop)
stat — таблица онлайн пользователей
- id
- date — дата подключения
- name — ФИО пользователя
- realaddress — реальный ip адрес
- virtualaddress — адрес выданный OpenVPN сервером
Создаем таблицы:
psql -Upgsql vpn;
create table users (
"id" serial,
"login" varchar(32) not null,
"name" varchar(32) not null,
"password" varchar(32) not null
"groups_id" integer not null,
"active" boolean not null default true,
"hwkey" boolean not null default true,
);
create table groups (
"id" serial,
"groupname" varchar(32) not null,
"active" boolean not null default true
);
create table log (
"id" serial,
"date" timestamp,
"users_id" integer not null,
"realaddress" varchar(32),
"virtualaddress" varchar(32),
"status" varchar(32)
);
create table stat (
"id" serial,
"date" timestamp,
"login" varchar(32);
"realaddress" varchar(32),
"virtualaddress" varchar(32)
);
Создам пользователя с правами чтения для таблиц users и groups, и записью в log и stat
psql -Upgsql template1
create user ovpn WITH PASSWORD 'password';
\q
psql -Upgsql vpn
grant select on users TO ovpn;
grant select on groups to ovpn;
grant all on log to ovpn;
grant all on stat to ovpn;
\q
Заполнение таблиц:
Для примера, создам две группы, admins с полным доступом к внутренней сети и rdp с доступом к серверам по RDP.
Соответсвенно создам двух пользователей:
100101 — админ
100102 — с доступом к RDP
psql -Upgsql vpn
insert into groups values(dafault,'admins',true);
insert into groups values(default,'rdp',true);
insert into users values(default,100101,'Иванов И.И.',md5('password'),(select id from groups where groupname = 'admins'),true,false);
insert into users values(default,100102,'Петров П.П.',md5('password'),(select id from groups where groupname = 'rdp'),true,false);
В результате, создались две группы с id 1 и 2. В соответствии с этими id будут созданы tables в фаерволе, к которым в свою очередь будут определены соответствующие разрешения.
Сертификаты
Если еще нет корневого сертификата, его нужно создать. У меня, сертификаты находятся в /root/ca
cd /root/ca
openssl req -x509 -newkey rsa:1024 -keyout /root/ca/ssl.key/ca.key -out /root/ca/ssl.crt/ca.crt -days 9999 -nodes -subj "/C=RU/ST=MSK/L=MSK/O=COMPANY/CN=CA"
Сертификат для сервера подписанный CA сертификатом:
openssl req -new -newkey rsa:1024 -nodes -keyout /root/ca/ssl.key/ovpn.key -subj /CN=ovpn.domain.ru -out /root/ca/ssl.csr/ovpn.csr
openssl ca -config ca.conf -in /root/ssl.csr/ovpn.csr -out /root/ssl.crt/ovpn.crt -batch
ovpn.key, ovpn.crt и ca.crt необходимо скопировать на OpenVPN сервер.
Для генерации пользовательских сертификатов я использую следующий скрипт, он упаковывает ключ и сертификат пользователя в запароленный p12 файл.
#!/usr/bin/perl
use Getopt::Long;
GetOptions ('a=s' => \$action, 'u=s' => \$user, 'O=s' => \$ou, 'o=s'=> \$options, 'help' => sub {HelpMessage()});
# variables
$ca='ca';
$ca_dir='/root/ca/';
$key_id='1000';
if (length($action)==0){
HelpMessage();
exit;
}
if ($action=~/^help$/){
print "actions:
adduser # Add a new User certificate
gen_revoke # Generate revoke file\n";
exit;
}
if ($action=~/^adduser$/){
adduser();
}
if ($action=~/^gen_revoke$/){
gen_revoke();
}
sub HelpMessage {
print "usage: ".$0. " -a <adduser|gen_revoke> -u <login> -O <VPN>\n";
exit;
}
sub adduser {
$p12_password = randomPassword(6);
print "~~Add User(Soft)~~\n";
if (length($user)==0 || length($ou)==0){
print "Error, User and OU must be\n";
exit;
}
# create cert
print "create User certificate\n";
system(`openssl req -new -newkey rsa:1024 -nodes -keyout $ca_dir/ssl.key/$user.key -subj /CN=$user/OU=$ou -out $ca_dir/ssl.csr/$user.csr`);
# sign USER cert
print "sign certificate\n";
system(`openssl ca -config ca.conf -in $ca_dir/ssl.csr/$user.csr -out $ca_dir/ssl.crt/$user.crt -batch`);
# p12
print "create p12\n";
system(`openssl pkcs12 -export -in $ca_dir/ssl.crt/$user.crt -inkey $ca_dir/ssl.key/$user.key -certfile $ca_dir/ssl.crt/$ca.crt -out p12/$user.p12 -passout pass:$p12_password`);
print "p12_password: ".$p12_password."\n";
# unlink files
unlink('$ca_dir/ssl.csr/$user.csr','$ca_dir/ssl.crt/$user.crt','$ca_dir/ssl.key/$user.key');
}
sub gen_revoke {
# gen CRL
system(`openssl ca -config $ca_dir/ca.conf -gencrl -crldays 365 -out $ca_dir/ssl.crl/certPEM.crl`);
system(`openssl crl -in $ca_dir/ssl.crl/certPEM.crl -outform DER -out $ca_dir/ssl.crl/certDER.crl`);
print "pls copy ./ssl.crl/certDER.crl to OpenVPN server\n";
}
sub randomPassword {
$password;
$_rand;
$password_length = $_[0];
if (!$password_length) {
$password_length = 10;
}
@chars = split(" ",
"A B C D E F G H I J K
L M N O P Q R S T U V
W X Y Z a b c d e f g
h i j k l m n o p q r
s t u v w x y z
0 1 2 3 4 5 6 7 8 9");
srand;
for (my $i=0; $i <= $password_length ;$i++) {
$_rand = int(rand 41);
$password .= $chars[$_rand];
}
return $password;
}
Создадим сертификаты для наших пользователей
/root/ca/gen_cert.pl -a adduser -U 100101 -O VPN
/root/ca/gen_cert.pl -a adduser -U 100102 -O VPN
на выходе получатся два файла /root/ca/p12/100101.p12 и /root/ca/p12/100102.p12 и пароли к ним, которые скрипт напечатает в консоли. Эти файлы нужно будет установить на пользовательских компьютерах (планшетах/телефонах) в защищенное хранилище. Как правило пользователям пароли от контейнеров не сообщаются, это исключает возможность пользователям передавать другим лицам свои ключи.
сразу же можно создать файл отозванных сертификатов crl
/root/ca/gen_cert.pl -a gen_revoke
Его необходимо скопировать на OpenVPN сервер.
Настройка сервера OpenVPN
port 1194
proto udp
dev tun
ca /usr/local/etc/ssl/ca.crt
cert /usr/local/etc/ssl/ovpn.crt
key /usr/local/etc/ssl/ovpn.key
crl-verify /usr/local/etc/ssl/certPEM.crl
dh /usr/local/etc/ssl/dh2048.pem
tls-verify "/usr/local/etc/openvpn/scripts/intra.pl tls-verefy"
topology subnet
server 172.16.40.0 255.255.255.0
push "route 172.16.0.0 255.255.0.0"
push "dhcp-option DOMAIN domain.local"
push "dhcp-option DNS 172.16.38.10"
client-connect /usr/local/etc/openvpn/scripts/intra.pl
client-disconnect /usr/local/etc/openvpn/scripts/intra.pl
auth-user-pass-verify "/usr/local/etc/openvpn/scripts/intra.pl auth-user-pass-verify" via-env
keepalive 10 120
persist-key
persist-tun
status /var/log/openvpn-status.log
log /var/log/openvpn.log
log-append /var/log/openvpn.log
management localhost 7505
verb 3
Итак, в четырех местах в конфиге вызывается внешний скрипт:
tls-verify параметр который позволяет проверять сертификат клиента на некоторое соответствие. В моем случае я проверяю что в CN записано тоже самое что клиент передает в Login, иначе говоря проверяю чтобы CN = Login. Это позволяет исключить возможность того, что пользователь имеющий один сертификат попытался бы зайти под другим пользователем введя его логин и пароль. Так же я проверяю чтобы в поле OU было значение VPN. Оба значения я добавляю при генерации сертификатов пользователей.
client-connect вызывается скрипт на этапе подключения пользователя. Тут я добавляю адрес пользователя в соответствующую таблицу фаервола, и делаю записи в таблицы log и stat.
client-disconnect вызывается скрипт на этапе отключения пользователя. Удаляю записи из фаервола, делаю записи в таблицы log и stat.
auth-user-pass-verify проверка переданого логина и пароля в БД.
Собственно, ниже сам скрипт
#!/usr/bin/perl
use DBI;
use Digest::MD5 qw(md5_hex);
$dbh=DBI->connect("DBI:Pg:dbname=vpn;host=localhost","ovpn","password");
($script_type,$common_name,$ifconfig_pool_remote_ip,$untrusted_ip) = ($ENV{'script_type'},$ENV{'common_name'},$ENV{'ifconfig_pool_remote_ip'},$ENV{'untrusted_ip'});
if ($script_type eq "client-connect") {
insert_to_firewall_group();
logging('start');
}
if ($script_type eq "client-disconnect") {
delete_from_firewall_group();
logging('stop');
}
if ($script_type eq "tls-verefy"){
tls_verefy();
}
if ($script_type eq "auth-user-pass-verify"){
auth_user_pass_verefy();
}
sub get_group {
my $req="SELECT groups.id
FROM groups
INNER JOIN users ON (users.groups_id = groups.id)
WHERE users.login='$common_name'";
@row = $dbh->selectrow_array($req);
}
sub insert_to_firewall_group {
get_group();
`/sbin/ipfw table 0 add $untrusted_ip`;
`/sbin/ipfw table $row[0] add $ifconfig_pool_remote_ip`;
}
sub delete_from_firewall_group {
get_group();
`/sbin/ipfw table 0 delete $untrusted_ip`;
`/sbin/ipfw table $row[0] delete $ifconfig_pool_remote_ip`;
}
sub tls_verefy {
($script_type, $depth, $x509) = @ARGV;
@X509=split(",",$x509);
$X509[0] =~s/^OU=//g;$ou = $X509[0]; $X509[1] =~s/^ CN=//g; $cn = $X509[1];
@ous=('VPN');
if ($depth == 0) {
#verefy OU
foreach(@ous){
if ($_ eq $ou) {
$ou_status = 1;
#exit 0;
}
}
#verefy CN
$req = "SELECT login FROM users WHERE login = '$cn' AND active = true";
@row = $dbh->selectrow_array($req);
if ($row[0] eq $cn) {
$cn_status = 1;
}
if ($ou_status == 1 && $cn_status == 1){
exit 0;
}
exit 1;
}
}
sub logging {
($status) = @_;
$date = `date '+%Y-%m-%d %H:%M:%S'`;
chop $date;
$req = "INSERT INTO log
VALUES(DEFAULT,'$date',(SELECT id FROM Users WHERE login='$common_name'),'$untrusted_ip','$ifconfig_pool_remote_ip','$status')";
$dbh->do($req);
if ($status eq "start"){
$st = "INSERT INTO stat
VALUES(DEFAULT,'$date','$common_name','$untrusted_ip','$ifconfig_pool_remote_ip')";
$dbh->do($st);
}
else {
$st = "DELETE FROM stat WHERE login='$common_name'";
$dbh->do($st);
}
}
sub auth_user_pass_verefy {
$username = $ENV{'username'};
$password = $ENV{'password'};
$common_name = $ENV{'common_name'};
$q_hw = "SELECT hwkey FROM users
WHERE login = '$common_name'
AND active = true";
@row = $dbh->selectrow_array($q_hw);
if ($row[0] == 1 ){
exit 0;
}
$password = md5_hex($password);
$req = "SELECT login
FROM users
WHERE login = '$username'
AND password = '$password'
AND active = true";
@row = $dbh->selectrow_array($req);
if ($row[0] eq $username) {
exit 0;
}
exit 1;
}
Фаервол
В файрволе созданы таблицы где номер таблицы = id группы из таблицы groups
#!/bin/sh
ipfw='/sbin/ipfw -q'
clients_real_ip="table(0)" # сюда заношу реальные адреса клиентов, на всякий случай
admins="table(1)" # таблица для группы admins
rdp="table(2)" # таблица для группы rdp
${ipfw} flush
# -- опускаю стандартные привила --
${ipfw} add allow ip from ${admins} to any # разрешаем группе админов все
${ipfw} add allow tcp from ${rdp} to any 3389 # разрешаю группе rdp доступ к RDP
${ipfw} add deny all from ${rdp} to any # запрещаю все остальное группе rdp
# -- другие правила --
осталось на пользовательском устройстве установить p12, скопировать ca.crt и такой конфиг
client
dev tun
proto udp
remote ovpn.domain.ru 1194
resolv-retry infinite
nobind
persist-key
persist-tun
script-security 3
ca "C:\\Program Files\\OpenVPN\\config\\ca.crt"
cryptoapicert "SUBJ:100101"
auth-user-pass
comp-lzo
log "C:\\Program Files\\OpenVPN\\log\\client.log"
log-append "C:\\Program Files\\OpenVPN\\log\\client.log"
verb 3
route-delay 5 30
tap-sleep 5
Напоследок, вкусняшка для тех кто пользуется дашбордом dashing. Скрипт нужно положить в dashboard/jobs
vpn_stat.rb
require 'pg'
require 'geoip'
$conn = PG.connect( :hostaddr=>'ovpn.domain.local', :user=>'ovpn',:password=>'password',:dbname=>'vpn')
def getVPNstat
all = Hash.new({ value: 0 })
result = $conn.exec( "SELECT COALESCE(users.name,stat.login) AS name,stat.realaddress AS ip FROM stat,users WHERE stat.login = users.login")
result.each do | row |
user = row['name']
user = user[0..8].downcase
ip = row['ip']
country = geoip(ip)
all[user] = {label: user,value: country }
end
send_event('vpnstat', { items: all.values })
end
def geoip(ip)
c = GeoIP.new('/usr/local/www/ruby/dashboard/geoip/GeoIP.dat').country(ip)
country = c[4]
return country
end
SCHEDULER.every '20s' do
getVPNstat
end
Скачать базу GeoIP:
wget -N http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
и распаковать ее в dashboard/geoip/.
На дашборд добавить плитку:
<li data-row="1" data-col="5" data-sizex="1" data-sizey="2">
<div data-id="vpnstat" data-view="Currency" data-unordered="true" data-title="VPN" style="background-color:green"></div>
</li>
Выглядет это будет так:

Вот и все. Осталось разве что написать нехитрую веб админку для самых ленивых.
Спасибо за внимание.