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>
Выглядет это будет так:
Вот и все. Осталось разве что написать нехитрую веб админку для самых ленивых.
Спасибо за внимание.
Комментарии (9)
vviz
19.03.2017 16:39Напоминает изобретение велосипеда, ИМХО.
Привязываем OpenVPN к базе AD через openvpn-plugin-auth-pam.so и живем спокойно.
Не замучаетесь на личных уст-вах пользователей ставить сертификаты? Особенно для планктона.
Правильнее, ИМХО, один сертификат на всех + пароль/логин при подключении.
Какой смысл создавать под каждого пользователя правило в пакетном фильтре?
Правильнее, сертификаты и ключи пользователей прописывать в файле конфига пользователя, как уже было предложено ранее.
Используйте tls-auth для предотвращения тупого долбления на сервер.
Затем, все пользовательские данные — инсталлятор клиента, конфиг, RDP файлы сворачиваются в самораспаковывающийся архив со скриптом, переписывающим все в нужные каталоги на компе пользователя и выкладывается на FTP/HTTP сервер.borisovEvg
19.03.2017 23:52Напоминает изобретение велосипеда, ИМХО.
Привязываем OpenVPN к базе AD через openvpn-plugin-auth-pam.so и живем спокойно.
Если кто нибудь напишет модуль openvpn-plugin-auth-sql.so это перестанет быть велосипедом?!
Вы тем кто скрещивает exim с psql или mysql тоже советуется не выдумывать велосипед и прикручивать его к ldap? Модуль то есть такой. И как быть тогда тем, у кого нет AD, поднимать его специально для хранения учеток?
Но посыл этой статьи не в том, где по вашему мнению должны хранится учетки, а в том что Openvpn(и не только он), зачастую позволяет отойти от шаблонов, и реализовать то, чего требую обстоятельства…
Правильнее, ИМХО, один сертификат на всех + пароль/логин при подключении.
Все от задач. Где-то можно вообще в общий доступ все выложить. где то нужны ключи зашитые в eToken
Какой смысл создавать под каждого пользователя правило в пакетном фильтре?
Тот же ответ. Разным группам сотрудников нужны разные уровни доступа. Кому-то к одному сайту, кому-то к другому сайту, почте, сетевым ресурсам и тп.
vviz
20.03.2017 00:15Совершенно верно, OpenVPN и иже сним позволяют решать множество задач. Если Вы ставили своей задачей составить хитроумное решения для тренировки интеллекта, то Вы сделали это прекрасно. Если же Вы ставили целью решить определенную задачу для продакшена, то извините, вычурно и избыточно, ИМХО.
И да, поднять контроллер AD стоит даже для такой задачи, виртуализация возможна сейчас практически на любых плаформах и ресурсах. Кроме всего прочего, можно использовать для авторизиции для Squid, Socks, Wi-Fi и т.д.selivanov_pavel
21.03.2017 16:02поднять контроллер AD стоит даже для такой задачи, виртуализация возможна сейчас практически на любых плаформах и ресурсах
То есть всегда стоит добавить например в linux-only инфраструктуру ещё один тип ОС, заводить для него мониторинг, установку обновлений безопасности, обеспечение отказоустойчивости, интеграцию с DNS? И ещё заплатить за это денег
ALexhha
20.03.2017 14:05+2И да, поднять контроллер AD стоит даже для такой задачи, виртуализация возможна сейчас практически на любых плаформах и ресурсах
а деньги за лицензии кто будет платить? АД как бы платный. Тогда уж смотреть в сторону OpenLDAP.
Если Вы ставили своей задачей составить хитроумное решения для тренировки интеллекта, то Вы сделали это прекрасно
вполне реальная задача, имхо. У меня было нечто подобное на AWS. Когда мы делали единую точку входа на нескольких аккаунтах, на каждом из которых использовались по 2-3 региона. И вот там надо было на основе имени пользователя давать/не давать права доступа в определенный аккаунт/регион/подсеть/vpc.
TerAnYu
Старайтесь избегать использования абсолютных путей.
Вместо
Можно записть его прямо в файл конфигурации:
Есть вероятность что в вашем случае такое не заработает.
Вот это тоже нет необходимости указывать.
Можно это указать так:
Хотя имеет ли смысл указывать логи, ведь они сами называются согласно имени файла конфигруации.
Файли имеет имя client_office_tcp.ovpn, то файл лога будет называться client_office_tcp.log в папке log.
И да, "web-админка" уже есть: https://github.com/furlongm/openvpn-monitor
По крайней мере она меня устраивает.
TerAnYu
Либо логи указать так:
borisovEvg
В официальном гайде предлагают использовать абсолютные пути, но если это так работает тоже, ок! пусть будет так.
P.S. В новых версиях похоже эта директива вообще игнорируется, предполагаю что значения берутся из реестра, и настраиваются через GUI
Судя по описанию это монитор. Но спасибо за наводку. Говоря про админку, я имел в виду написания приложения через которое можно было бы управлять пользователями(добавлять, отключать и тп), правилами, группами и т.д.
TerAnYu
Просто использование путей уменьшает универсальность конфига, его невозможно (наверное) использовать в других ОС системах без изменений.
Официальные гайды, похоже, мало когда актуализируют.
Ну да, это монитор, поэтому и написал в кавычках.
Всё же использование ldap как-то более универсально, чем использование велосипедов, но это дело вкуса.