Захотелось собрать VPN-комбайн который бы пользователей брал из БД, настраивал фаервол под этого пользователя и писал логи в БД.
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 filen";
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 ben";
exit;
}
# create cert
print "create User certificaten";
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 certificaten";
system(`openssl ca -config ca.conf -in $ca_dir/ssl.csr/$user.csr -out $ca_dir/ssl.crt/$user.crt -batch`);
# p12
print "create p12n";
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 servern";
}
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>
Выглядет это будет так:
Вот и все. Осталось разве что написать нехитрую веб админку для самых ленивых.
Спасибо за внимание.
Автор: borisovEvg