Cisco Downloadable ACL (ACSACL) & FreeRADIUS
Предполагается, что читатель/внедренец ориентируется в принципах работы протокола RADIUS и конфигурирования FreeRADIUS-server, и имеет опыт эксплуатации одного из девайсов линейки PIX/ASA/FTD/ISR (либо имеет доступ к телу для надругательства). А начнем, как это принято в приличном обществе, с терминологии.
Когда речь заходит о Remote Access VPN на базе решений Cisco, обязательно всплывает вопрос ограничения доступа удаленных пользователей и централизованного управления оным. Следует отдать должное спецам компании - начиная с VPN3000, PIX, IOS с классическим EzVPN, и заканчивая ASA/FTD с AnyConnect, несмотря на изменения в протоколах доступа и усложнение функционала, протоколы авторизации остались прежними (естественно, обрастая дополнительными возможностями).
В частности, речь идет о Downloadable ACL - наборе ACE (Access Control Entry или Access Control Rule), передаваемом на PIX/ASA RADIUS-сервером вместе с Access-Accept пакетом в составе Cisco RADIUS VSA. Формат для EzVPN/AnyConnect следующий:
Cisco-AVPair += "ip:inacl#<number>=ACE"
где number=0..999999999, а ACE - правило в формате extended ACL PIX/IOS. Возможны еще вариации на тему Clientless подключений - там формат ACE немного иной, но принцип не меняется.
IOS использует wildcard формат маски, характерные команды и принципы конфигурации и отладки. Но тоже поддерживает обсуждаемый в статье функционал (хотя и не для всех доступных на нем типов RA VPN). А PIX/ASA, в свою очередь, поддерживают ACL в формате IOS. Но для экономии места и нервов, оба нюанса в статье опущены.
Таким образом, клиенту передается массив данных, вида:
ip:inacl#1=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 3389
ip:inacl#10=permit tcp 10.0.0.0 255.0.0.0 10.0.0.0 255.0.0.0 eq 3389
ip:inacl#11=permit udp 10.0.0.0 255.0.0.0 any eq 53
...
ip:inacl#99=deny ip any any
в составе атрибута Cisco-AVPair (это первый вариант загрузки). Последнее deny тут исключительно для мебели (для иллюстрации последнего ACE) и применяется неявно по-умолчанию. Все это выглядит именно так, пока RADIUS пакет с ACL не превышает 4KB - наследие RFC2865 и принципа обратной совместимости, согласно которому старые клиенты, не знающие об RFC6613/7930 не должны иметь проблем с новыми реализациями сервера (впрочем и не будут иметь, пока RFC7930 не выйдет из статуса Experimental).
При превышении лимита в 4KB начинается... для начала - начинается путанница в терминологии. Поскольку для Challenge-Response механизма передачи ACL клиенту (это второй вариант), отдельного названия не предусмотрено. Для поиска по теме лучше использовать термин "ACSACL", поскольку по запросу "Downloadable ACL", "ip:inacl" и "Cisco-AVPair ACL" вываливается в основном инфа по первому варианту. В контексте PIX/ASA, сама Cisco оперирует терминами "Cisco-AV-Pair ACL" и "Downloadable ACL" ("DACL") для первого и второго вариантов соответственно. Но только пока речь идет об обоих сразу - если в документации речь идет только о первом, его не заморачиваясь называют "Downloadable ACL". Я буду использовать PIX-овую терминологию в сокращении - cavp-acl и dacl соответственно.
Подробное описание процесса имеется в Cisco ASA Series CLI Configuration Guide, до версии 9.1 включительно - в поздних версиях структура документации изменилась с уклоном в "понижение порога вхождения", а подробности о реализации dACL из нее испарились.
Итак, способ передачи over-4K ACL основан на штатном для RADIUS механизме Access-Challenge.
Обычный клиент-серверный обмен по RADIUS протоколу инициируется клиентским запросом Access-Request и завершается ответом сервера Access-Accept; или Access-Reject, если пользователь не прошел аутентификацию/авторизацию.
Если серверу потребовался дополнительный обмен с клиентом, вместо ответа Access-Accept отправляется отклик Access-Challenge с установленным атрибутом State RFC2865 - маркером этапа обмена.
Клиент обрабатывает данные от сервера и возвращает необходимые данные в составе повторного запроса Access-Request. Клиент так-же копирует атрибут State из отклика Access-Challenge сервера для того, чтобы сервер мог определить - в ответ на какой из Access-Challenge пришел данный Access-Request.
Сервер может повторить итерацию Access-Challenge -> Access-Request необходимое число раз, либо завершить обмен ответом Access-Accept; или Access-Reject - если посчитает результаты обмена неудовлетворительными.
Нужно понимать, что собственно данные в клиент-серверном обмене, как и критерии завершения сессии (Accept/Reject), никак не регламентируются стандартом. Это может быть многофакторная аутентификация, когда клиент поэтапно передает серверу данные аутентификации пользователя. Либо это может быть передача данных авторизации в несколько этапов - например большого ACL для уже установленной сессии (для пользователя, прошедшего аутентификацию). В этом случае клиент не передает на сервер данные аутентификации - только имя авторизуемого пользователя и атрибут State.
Строго говоря второго "может" как раз таки быть не может - передача параметров авторизации вне диалога аутентификации называется CoA и регламентируется отдельным стандартом. Да и реализуется несколько иначе, чем тут описывается. Но мы все-таки больше о проприетарной самодеятельности, так что будем считать, что все-таки "может".
В деталях алгоритм запроса/передачи dACL выглядит следующим образом (в формате лога FreeRADIUS; оставлены только относящиеся к теме атрибуты):
Received Access-Request Id 114 from client to server
User-Name = "username"
User-Password = "password"
ASA-TunnelGroupName = "DefaultWEBVPNGroup"
Sent Access-Accept Id 114 from server to client
Cisco-AVPair += "merge-dacl=after-avpair"
Cisco-AVPair += "ip:inacl#1=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 3389"
Cisco-AVPair += "ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-username-1556031788"
стандартный обмен Access-Request - Access-Accept - собственно аутентификация пользователя "username" с паролем "password".
Обратите внимание на параметры авторизации, передаваемые клиенту (возвращаемые сервером в Access-Accept):
- ip:inacl#1... - cavp-acl (ACL по первому варианту);
- ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-username-1556031788 - имя dACL - (ACL по второму варианту); Формат атрибута жестко фиксирован:
#ACSACL#-IP-<user-name>-<unix-timestamp>
где user-name - имя пользователя, для которого запрашивается dacl, а unix-timestamp - время последнего изменения ACL в формате unixtime (используется клиентом для кэширования dacl).
- merge-dacl=after-avpair - указание клиенту консолидировать cavp-acl и dacl в один ACL; порядок объединения понятен из контекста (возможен еще before-avpair).
ASA имеет такую директиву в конфигурации - в блоке "aaa-server RADIUSSERVER protocol radius", а атрибут не понимает и потому игнорируют.
FTD (версии 6.3.0 по крайней мере) директивы в конфигурации не имеет, а атрибут... тоже игнорирует - с печальными последствиями, понятными из отладочного сообщения: "dACL processing skipped: dACL would be overridden by AV-Pairs". Хотя и не должна согласно документации. Впрочем документация в этом аспекте не отличается излишней детальностью, так что возможно атрибут передается как-то иначе (хотя куда уж иначе?...), а возможно будет допилено в следующих версиях. Этот нюанс разберем ниже - после конфигурации FreeRADIUS.
Received Access-Request Id 115 from client to server
User-Name = "#ACSACL#-IP-username-1556031788"
User-Password = "password"
Cisco-AVPair = "aaa:service=vpn"
Cisco-AVPair = "aaa:event=acl-download"
Message-Authenticator = 0xe97966ff793aa243c1157221ce187b20
Sent Access-Challenge Id 115 from server to client
Cisco-AVPair += "ip:inacl#10=permit tcp 10.0.0.0 255.0.0.0 10.0.0.0 255.0.0.0 eq 3389"
Cisco-AVPair += "ip:inacl#11=permit udp 10.0.0.0 255.0.0.0 any eq 53"
...
Cisco-AVPair += "ip:inacl#47=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 389"
State = 0x3437
WARNING: (2) Packet is large, and possibly truncated - 4061 vs max 4096
дополнительный обмен - запрос и передача dacl. Таких сегментов/итераций Challenge-Response может быть несколько - в зависимости от общего размера dACL.
- Запрос:
- User-Name/User-Password - в качестве имени пользователя передается собственно имя dACL. При этом в качестве пароля используется таковой из последнего (связнанного с текущим) запроса аутентификации.
Представленные логи сняты с обмена между Cisco ASA и FreeRADIUS - FTD атрибут с паролем при авторизации не передает, что логично - на практике бывает и двухфакторная аутентификация и тогда придет пароль из второго фактора (OTP), а его обычно нельзя использовать повторно. По этой причине, пароль в запросе авторизации не несет вообще никакой функциональности.
- aaa:service=vpn, aaa:event=acl-download - атрибуты-маркеры dACL запроса, т.е. их наличие как раз и отличает dACL запрос авторизации от запросов аутентификации - различать их на основе структуры имени пользователя не слишком надежно.
- Message-Authenticator - ну собственно он и есть - "Message-Authenticator"; согласно RFC 2869 - HMAC-MD5 функция от содержимого пакета и shared secret клиента. По мнению Cisco: "The presence of the Message-Authenticator attribute prevents malicious use of a downloadable access list name to gain unauthorized network access.". А по RFC 2869 - предназначен для защиты от спуфинга Access-Request с хэшированными паролями или с тунеллированием (EAP). Т.е. позволяет удостовериться, что пакет сгенерирован NAS-ом, а не кем-то другим. История появления здесь этих двух наборов атрибутов (aaa: и Message-Authenticator) тянется аж в эпоху PIX/VPN 3000, когда добавление функционала dACL создало потенциальную уязвимость, позволявшую аутентифицироваться с использованием предварительно перехваченного имени dACL, из-за чего и потребовалось ввести атрибуты, надежно отличающие запросы авторизации от запросов аутентификации (по указанной выше причине, пароль в таких запросах проверять бесполезно). За давностью лет (начало 2000-х) на cisco.com данной информации не сохранилось, но следы в инете на тему "Field Notice: FN - 61965 - CS ACS for Windows Downloadable IP Access Control List Vulnerability" найти можно. Очевидное решение - тупо заблокировать аутентификацию по маске формата имени dACL на NAS, или хотя-бы предоставить такую возможность эксплуатанту, видимо не рассматривали как противоречащее стандарту.
И кстати о стандартах - с этой т.з. нужно отметить определенную корявость этой (Cisco-вской) реализации. Дело в том, что запрос авторизации пользователя в атрибуте User-Name должен содержать... очевидно, имя авторизуемого пользователя (об идентификаторах сессии - в конце, в описании CoA). А нестандартный параметр авторизации (коим является имя dACL), должен содержаться в атрибуте, не зарезервированном стандартами протокола RADIUS (для этого как раз и придумали VSA). И в Cisco даже последовали этим путем - со стороны сервера имя dACL передается как Cisco-AVPair == "ACS:CiscoSecure-Defined-ACL=". Но на пол пути передумали, и со стороны клиента запрос передается уже с User-Name=.
Оправданием для них служит тот факт, что указанные в стандартах обязательные требования к запросам авторизации (наличие Message-Authenticator в частности), допустимы к использованию в запросах аутентификации. Т.е. гарантированного способа отличить одни от других на стороне сервера (как и ответы на них на стороне клиента) просто не существовало на момент реализации. Впрочем, оправдание это довольно слабое - Cisco обычно сама активно участвует в разработке стандартов, если это касается их продукции. Потому и не слишком понятна подобная отсебятина в данном конкретном случае...
Сейчас уже есть CoA и связанные с ним значения "Authorize Only" и "Additional-Authorization" атрибута Service-Type, но они появились на свет в контексте предложений по Dynamic Authorization Extensions (RFC3576) только в июле 2003г., а в качестве стандарта предложено в апреле 2019 - RFC8559 (этот набор RFC как раз и описывает процедуры авторизации, в т.ч. многоступенчатые). Но как показано в описании CoA в конце трактата, Cisco и здесь похерила стандарты в лучших традициях обратной совместимости (ну тут уж и правда деваться некуда - или сохранять совместимость, или полностью менять реализацию).
- Отклик: - собственно набор ACE в составе Cisco-AVPair; - маркер этапа обмена State. Стандарт не регламентирует формат содержимого - просто набор октетов; клиент просто копирует его в следующий запрос без интерпретации, а сервер определяет по нему порядок запроса в последовательности Challenge-Response. В данном случае, в атрибуте содержится номер следующего ACE, предназначенного к передаче клиенту (подробнее в коде модуля).
Так же видно отладочное предупреждение об опасно близком к 4KB размере пакета (спровоцировано для наглядности, установкой большого размера сегмента ACL - подробнее в коментариях к коду модуля).
Received Access-Request Id 116 from client to server
User-Name = "#ACSACL#-IP-username-1556031788"
User-Password = "password"
State = 0x3435
Cisco-AVPair = "aaa:service=vpn"
Cisco-AVPair = "aaa:event=acl-download"
Message-Authenticator = 0x8bab76f1aca7e0705f012cdcd0c6fa55
Sent Access-Accept Id 116 from server to client
Cisco-AVPair += "ip:inacl#48=10.0.0.0 255.0.0.0 any eq 443"
...
Cisco-AVPair += "ip:inacl#57=deny ip any any"
финальный обмен - оставшиеся ACE укладываются в лимит 4KB. Сервер сообщает об этом ответом Access-Accept без атрибута State.
Received Accounting-Request Id 117 from client to server
User-Name = "username"
Acct-Status-Type = Start
Acct-Delay-Time = 0
Acct-Session-Id = "CEA00093"
Acct-Authentic = RADIUS
Received Accounting-Request Id 118 from client to server
User-Name = "usename"
Acct-Status-Type = Stop
Acct-Delay-Time = 0
Acct-Input-Octets = 46100
Acct-Output-Octets = 10621
Acct-Session-Id = "CEA00093"
Acct-Authentic = RADIUS
Acct-Session-Time = 155
Acct-Input-Packets = 317
Acct-Output-Packets = 11
Acct-Terminate-Cause = User-Request
клиент реагирует на это отправкой аккаунтинг-старт пакета, в обычном случае присылаемым сразу после аутентификации. Ниже следует аккаунтинг-стоп пакет завершения сессии - т.е. disconnect пользователя. Accaunting обмен приведен просто для полноты картины - непосредственно к передаче dACL он отношения не имеет.
На этом с теорией всё (ну почти) - далее только реализация алгоритма на FreeRADIUS с необходимыми и пространными пояснениями. Нюансы работы FreeRADIUS и собственно протокола RADIUS - на сайтах проекта, FreeRADIUS HowTos и коммерческой поддержки - последний так-же содержит общедоступную документацию. Ну и просто свободным поиском - за 20 лет развития продукта в сети накопилось изрядно информации. Вопросы синтаксиса perl я вообще раскрывать не буду - пользуясь изложенной инфой и документацией, желающие запросто перепишут модуль на своем любимом языке (и нет - perl не мой любимый, но для парсинга текста и наглядности вполне подходящий).
Ответ на вопрос "почему FreeRADIUS, а не ACE/ISE?" будет раскрыт попутно - по мере обсуждения. Но если коротко - на нем эта задача поддается решению в принципе. Т.е. почему бы и нет?...
Для начала, берем за образец example.pl модуль из FreeRADIUS (скопируем его как custom-module.pl) и дополняем под наши нужды:
- добавляем хэш конфигурации perl-модуля %RAD_PERLCONF - для передачи параметров в модуль:
# Bring the global hashes into the package scope
our (%RAD_REQUEST, %RAD_REPLY, %RAD_CONFIG, %RAD_STATE, %RAD_PERLCONF);
- заводим процедуру, реализующую формирование и передачу ACL клиенту и инициализируем переменные, необходимые для работы процедуры. Собственно, задача модуля (если мы не собираемся озадачиваться парсингом и валидацией ACE) - нарезать Control-атрибут с ACL на куски, заведомо меньшие 4KB, и возвращать их в составе Reply-атрибута в диалоге Access-Challenge. Поэтому он прост как карандаш:
sub acsacl {
$RAD_CHECK{'Auth-Type'} = 'Accept';
# атрибут, из которого процедура будет считывать ACE; по умолчанию Cisco-AVPair
my $load_aclattr = defined $RAD_PERLCONF{'ACL-attr'} ? $RAD_PERLCONF{'ACL-attr'} : "Cisco-AVPair";
# атрибут, в который процедура будет загружать ACE для отправки клиенту; по умолчанию Cisco-AVPair
my $send_aclattr = defined $RAD_PERLCONF{'Send-ACL-attr'} ? $RAD_PERLCONF{'Send-ACL-attr'} : "Cisco-AVPair";
# размер фрагмента ACL в байтах, по умолчанию 3900B (выше в логах был пример со значением 4060B)
my $sizelimit = defined $RAD_PERLCONF{'ACL-chunksize'} ? $RAD_PERLCONF{'ACL-chunksize'} : 3900;
if (defined $RAD_CHECK{$load_aclattr} and ref($RAD_CHECK{$load_aclattr}) eq 'ARRAY') {
my $offset = (defined($RAD_REQUEST{'State'})) ? $RAD_REQUEST{'State'} : '0x30';
$offset =~ s/^0x//;
$offset = pack("H*", $offset);
my $startpos = $offset;
my $size=20; # начальный размер пакета (соответствует размеру заголовка)
for (; $offset <= $#{$RAD_CHECK{$load_aclattr}} and ($size + 8 + bytes::length($RAD_CHECK{$load_aclattr}[$offset])) <= $sizelimit; $size += (8 + bytes::length($RAD_CHECK{$load_aclattr}[$offset])) and $offset++) {
push @{$RAD_REPLY{$send_aclattr}}, $RAD_CHECK{$load_aclattr}[$offset];
}
if ($offset != $#{$RAD_CHECK{$load_aclattr}}+1) {
&radiusd::radlog(L_DBG, "Challenge: ACL fragment - ACE from " . ($startpos+1) . " to " . $offset . " of " . ($#{$RAD_CHECK{$load_aclattr}}+1) . ", $size bytes summary for " . ${RAD_REQUEST{'User-Name'}} );
$RAD_REPLY{'State'} = '0x' . unpack('H*', $offset);
$RAD_CHECK{'Response-Packet-Type'} = "Access-Challenge";
return RLM_MODULE_HANDLED;
} else {
&radiusd::radlog(L_DBG, "Accept: last ACL fragment - ACE from " . ($startpos+1) . " to " . $offset . " of " . ($#{$RAD_CHECK{$load_aclattr}}+1) . ", $size bytes summary for " . ${RAD_REQUEST{'User-Name'}} );
return RLM_MODULE_OK;
}
} else {
&radiusd::radlog(L_DBG, "Downloadable ACL not defined ($#{$RAD_CHECK{$load_aclattr}}) for ${RAD_REQUEST{'User-Name'}}");
#push @{$RAD_REPLY{$send_aclattr}}, "ip:inacl#1=deny ip any any";
#return RLM_MODULE_OK;
return RLM_MODULE_NOOP;
}
}
Стоит отдельно остановиться на директиве Auth-Type := 'Accept'.
Явная установка атрибута Auth-Type в Accept категорически не рекомендуется. Обычно атрибут используется либо для выбора метода аутентификации, либо устанавливается в Accept/Reject модулями аутентификации по результатам проверки предоставленных клиентом данных. Но у нас-то аутентификации как раз и нет - чистая авторизация и никакой из модулей аутентификации, в общем случае, решения принять не может (обсуждалось выше на этапе 2). Так что выбор стоит только между Accept и Reject.
Вариант вернуть Access-Reject отпадает - NAS однозначно трактует такой ответ как ошибку загрузки ACL:
6 Apr 23 2019 21:03:01 109016 Can't find authorization ACL '#ACSACL#-IP-username-1556042581' on 'AAA server' for user 'username'
и не устанавливает никаких ACL (неважно - пришел ACL в составе ответа или нет). Соответственно, неявное правило deny не устанавливается и разрешает любой пользовательский трафик (в общем случае не любой, конечно - NAS может иметь свои настройки и правила для VPN трафика). Это нормальное поведение сервера, если он не имеет соответствующих пользователю dACL, либо не поддерживает такой функционал вовсе (в последнем случае запрос dACL - это запрос аутентификации несуществующего пользователя).
То-же самое произойдет и для Access-Accept без ACL, только не считается ошибкой:
6 Apr 23 2019 21:09:01 716049 Group <DfltGrpPolicy> User <username> IP <77.88.99.100> Empty SVC ACL.
Пользовательская сессия в обоих случаях останется активной, поскольку аутентифицирована отдельным/предыдущим запросом.
Единственным случаем запрета установления сессии, является ошибка парсинга ACL:
3 Apr 23 2019 21:13:19 109032 Unable to install ACL '#ACSACL#-IP-username-1556043199', downloaded for user username; Error in ACE: '10.0.0.0 256.0.0.0 any eq 65537'
6 Apr 23 2019 21:13:19 716051 Group <DfltGrpPolicy> User <username> IP <77.88.99.100> Error adding dynamic ACL for user.
6 Apr 23 2019 21:13:19 113033 Group <DfltGrpPolicy> User <username> IP <77.88.99.100> AnyConnect session not allowed. ACL parse error.
Т.е. будет разорвана уже аутентифицированная пользовательская сессия. Но нет худа без добра - нет нужды ваять собственный валидатор ACL.
Кстати, такой-же эффект может возникнуть при транкатенации ACL из-за превышения лимита 4KB:
3 Apr 23 2019 21:13:19 109032 Unable to install ACL '#ACSACL#-IP-username-1556043199', downloaded for user username; Error in ACE: 'permit udp 10.0.0.0 255.0.'
Таким образом, нужно всегда возвращать Access-Accept с непустым ACL (т.е. должен существовать Default/Common Authorization Profile, в терминах Cisco ACS/ISE), если только вы не исповедуете принцип "разрешено все, что не запрещено" (что, вообще-то, ни религией, ни здравым смыслом не запрещается - так что решать Вам). Либо озаботиться политикой запрета на стороне NAS. И да - в ip:inacl правилах, remark в гордом одиночестве не поддерживается для формирования непустого ACL (с ремаркой и имплицитным deny) - загрузка такого ACL сопровождается невнятной ошибкой:
3 Apr 23 2019 21:13:23 722046 Group <DfltGrpPolicy> User <username> IP <77.88.99.100> Session terminated: Unable to establish tunnel
и сопутствующим сообщением об "Internal Error". Т.е. remark допускается только в комбинации с явным permit/deny ACE.
Здесь углубление в потроха протокола таки потребуется... С т.з. пользователя FreeRADIUS, последовательность атрибутов одного типа - это массив (тип атрибута в терминах RFC2865 - это "что за атрибут" (его имя), а не тип данных в нем хранящийся (последний в том же rfc обозначен как "Data Type")). Так что далее, говоря об "элементах" атрибута я имею в виду именно последовательность атрибутов одного типа ("Cisco-AVPair" в данном случае), с т.з. Perl API FreeRADIUS представляющих собой хэш, с ключем - именем (типом) атрибута и массивом строк (в нашем случае) в качестве значения. И кстати, согласно того же RFC2865, порядок атрибутов в запросах/ответах гарантируется только при проксировании - ни сервер, ни клиент не должны ожидать сохранения порядка следования атрибутов в пакете (даже "одноименных"). Поэтому ACE и "тянут" порядковый номер "с собой"...
Откуда ACL извлекать и присваивать как значение атрибуту - тут каждый решает для себя сам - зависит от любимого хранилища. Можно в SQL базе (и практичнее всего), я предпочитаю в AD LDAP, в multivalued атрибуте (пользовательские и групповые ACL удаляются/блокируются вместе с пользователями/группами, ну и виндовым админам так рулить проще). Этот вопрос за рамками повествования (ибо накреативить тут можно на суровый по объему трактат). Только не стоит использовать модуль rlm_files - формат Users файла не допускает многострочного описания check (control) атрибутов. А указывать несколько десятков атрибутов с ACE в одну строчку... это будет та еще конфигурация.
- Собсно о логике модуля. Если атрибут ACL пустой - возвращаем RLM_MODULE_OK с заглушкой "deny ip any any" (или RLM_MODULE_NOOP, если полагаемся на политики NAS).
} else {
&radiusd::radlog(L_DBG, "Downloadable ACL not defined for ${RAD_REQUEST{'User-Name'}} - add stub (deny any) ACE");
#push @{$RAD_REPLY{$send_aclattr}}, "ip:inacl#1=deny ip any any";
#return RLM_MODULE_OK;
return RLM_MODULE_NOOP;
- Если атрибут ACL не пустой - подготавливаем исходные данные для формирования фрагмента - устанавливаем смещение $offset первого ACE текущего фрагмента. Для первого фрагмента $offset = 0, для последующих - извлекаем из атрибута State, куда оно сохраняется на предыдущей итерации. Perl оперирует строками (вообще - оно язык с динамической типизацией (тип определяется контекстом), но именно здесь - не тот случай). State - набор октетов (с точки зрения RADIUS сервера). Соответственно, в модуль он попадает как строка в шестнадцатиричном формате (0x..). Поскольку стандарт не регламентирует формат содержимого, оставляя его на усмотрение приложения (сервера), будем упаковывать в него смещение $offset. Для инициализации устанавливаем его в 0x30 (т.е. 0 в ASCII кодировке):
} else {
my $offset = (defined($RAD_REQUEST{'State'})) ? $RAD_REQUEST{'State'} : '0x30';
if ( substr( $offset, 0, 2 ) eq "0x" ) {
$offset =~ s/^0x//;
$offset = pack("H*", $offset);
}
- подготавливаем фрагмент ACL для пакета размером не более $sizelimit (3900Byte по-умолчанию). Рассчитать размер результирующего RADIUS пакета достаточно просто (RFC2865 и RADIUS Attributes Configuration Guide нам в помощь): 20B заголовок пакета + (8B заголовкок атрибута (2B + 6B на RFC и VSA заголовки атрибута)) + X Bytes данных )*N - где N количество атрибутов (в нашем случае - количество ACE)). При этом атрибут State (>=3B) формируется вне цикла добавления ACE, поэтому в рассчете его не учитываем, просто оставляя запас.
Так-же нужно учитывать возможность добавления атрибутов вне модуля - оставить запас и учитывать этот нюанс при составлении конфигурации FreeRADIUS, т.е. для всех атрибутов, добавляемых вне модуля, брать калькулятор и считать оверхед вручную (программными методами посчитать размер результирующего пакета с учетом уже имеющихся в ответе атрибутов довольно сложно, поскольку тип атрибута (и как следствие, его размер) в общем случае не известен ("внутри" сервера атрибуты уже "распарсены" с учетом типов)):
my $startpos = $offset; # переменная для логирования - первый элемент фрагмента
my $size=20; # начальный размер пакета (соответствует размеру RADIUS заголовка)
for (; $offset <= $#{$RAD_CHECK{$load_aclattr}} and ($size + 8 + bytes::length($RAD_CHECK{$load_aclattr}[$offset])) <= $sizelimit; $size += (8 + bytes::length($RAD_CHECK{$load_aclattr}[$offset])) and $offset++) {
push @{$RAD_REPLY{$send_aclattr}}, $RAD_CHECK{$load_aclattr}[$offset];
}
- если номер последнего ACE текущего фрагмента не соответсвует концу ACL (фрагмент не последний), используем отклик Access-Challenge; в противном случае - Access-Accept (по-умолчанию для Auth-Type == 'Accept'). Поскольку $offset инкрементируется на выходе из итерации, то после выхода из цикла его значение на единицу больше чем внутри него. Нас это устраивает - на данном этапе процедуры $offset - это первый элемент следующего фрагмента, а не последний текущего, но при сравнении и в логах нужно учитывать этот нюанс:
if ($offset != $#{$RAD_CHECK{$load_aclattr}}+1) {
&radiusd::radlog(L_DBG, "Challenge: ACL fragment - ACE from " . ($startpos+1) . " to " . $offset . " of " . ($#{$RAD_CHECK{$load_aclattr}}+1) . ", $size bytes summary for " . ${RAD_REQUEST{'User-Name'}} );
$RAD_REPLY{'State'} = '0x' . unpack('H*', $offset);
$RAD_CHECK{'Response-Packet-Type'} = "Access-Challenge";
return RLM_MODULE_HANDLED;
} else {
&radiusd::radlog(L_DBG, "Accept: last ACL fragment - ACE from " . ($startpos+1) . " to " . $offset . " of " . ($#{$RAD_CHECK{$load_aclattr}}+1) . ", $size bytes summary for " . ${RAD_REQUEST{'User-Name'}} );
return RLM_MODULE_OK;
}
__
По модулю особо больше обсуждать нечего - перейдем к конфигурации FreeRADIUS. За рамками обсуждения останутся вопросы аутентификации (ибо специфично для каждой конкретной инсталляции) и варианты хранения ACL (слишком богатое поле для творчества) - примеры будут в формате кофигурационных файлов FreeRADIUS.
Итак, наш пользователь:
#/etc/raddb/users
username Cleartext-Password := "password", ASA-TunnelGroupName =* ""
Cisco-AVPair += "ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-%{%{Stripped-User-Name}:-%{%{User-Name}:-None}}-%l"
Формат имени был рассмотрен выше. Здесь только один нюанс - в целях упрощения примера, для формирования имени dACL используется переменная "%l" - timestamp. Т.е. кэширование на стороне NAS (клиент) не работает (время создания dACL соответствует времени формирования ответа сервером). Процесс аутентификации и запроса dACL от NAS почти полностью будет соответствует примеру выше - отсутствуют только cavp-acl и соответственно merge-dacl:
Received Access-Request Id 121 from client to server
User-Name = "username"
User-Password = "password"
Sent Access-Accept Id 121 from server to client
Cisco-AVPair += "ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-username-1556032425"
Received Access-Request Id 122 from client to server
User-Name = "#ACSACL#-IP-username-1556032425"
User-Password = "password"
Cisco-AVPair = "aaa:service=vpn"
Cisco-AVPair = "aaa:event=acl-download"
Message-Authenticator = 0x6101a78c76855c2aa35454cf3b17cfb7
На данном этапе желательно выделить обработку запроса dACL в отдельный virtual-server - как было замечено выше, это уже чистая авторизация и мы должны быть твердо уверены, что не вернули Access-Accept на запрос аутентификации с именем пользователя в формате dACL:
#/etc/raddb/hints
DEFAULT NAS-IP-Address == "client_ip", Cisco-AVPair =~ "aaa:event=acl-download", Cisco-AVPair =~ "aaa:service=ip-admission|aaa:service=vpn", User-Name =~ "#ACSACL#-IP-(.*)-[0123456789]*", Message-Authenticator =* "", ASA-TunnelGroupName !* ""
Hint = "ACSACL",
User-Name := "%{1}@DACL"
Здесь мы убеждаемся, что запрос от NAS содержит характерные для запроса dACL атрибуты (и не содержит характерные для запроса на аутентификацию - ASA-TunnelGroupName), выделяем имя пользователя из имени dACL, и добавляем к нему realm=DACL (для последующего перехвата в файле proxy.conf).
Если обратить внимание на проверку Cisco-AVPair =~ "aaa:service=ip-admission|aaa:service=vpn", возникает вопрос - почему aaa:service проверяется на два значения (ip-admission и vpn)? Ответ кроется в Cisco ASA Series CLI Configuration Guide, где утверждается, что запрос dACL сопровождается парой атрибутов aaa:service=ip-admission и aaa:event=acl-download, установленным Message-Authenticator, да еще и в сопровождении "null password attribute". На деле, я никогда не видел подобного поведения ни от ASA, ни от FTD - запрос dACL всегда сопровождается парой aaa:service=vpn и aaa:event=acl-download, Message-Authenticator и непустым паролем (обсуждалось выше). Возможно это копипаста из документации к PIX или VPN3000 - под рукой для проверки таковых нет, а возможно просто отсебятина документописателей из Сisco - в любом случае верим на слово и проверяем оба значения.
Так же стоит остановиться на Message-Authenticator. Во FreeRADIUS есть возможность наглухо заблокировать запросы без Message-Authenticator (директива require_message_authenticator = yes), но применяется она на клиента в целом - на отдельные запросы её не повесить. А запросы аутентификации от ASA/FTD приходят без Message-Authenticator (наличие Message-Authenticator согласно RFC5080 обязательно только для запросов авторизации, т.е. как раз для запроса dACL). Так что проверяем наличие Message-Authenticator самостоятельно, а верификацию значения пусть проводит сервер в соответствии с RFC 2869 (согласно требованиям стандарта, при наличии Message-Authenticator в запросе, сервер обязан вычислить актуальное значение MA для пакета и молча отбросить его при несоответствии переданного значения вычисленному).
Указываем virtual_server = dacl, для обработки запросов с realm == DACL:
#/etc/raddb/proxy.conf
realm DACL {
virtual_server = dacl
hints
delimiter = @
format = suffix
}
и создаем предельно лаконичную конфигурацию для virtual_server == dacl (ну или как принято - ссылку на таковую в /etc/raddb/sites-available):
#/etc/raddb/sites-enabled/dacl
server dacl {
authorize {
load.dacl
if (Hint == "ACSACL") {
update control {
&Cisco-AVPair += "ip:inacl#10=permit tcp 10.0.0.0 255.0.0.0 10.0.0.0 255.0.0.0 eq 3389",
&Cisco-AVPair += "ip:inacl#11=permit udp 10.0.0.0 255.0.0.0 any eq 53",
...
&Cisco-AVPair += "ip:inacl#45=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 443",
&Cisco-AVPair += "ip:inacl#46=10.0.0.0 255.0.0.0 10.0.0.0 255.0.0.0 eq 443",
...
&Cisco-AVPair += "ip:inacl#57=deny ip any any"
}
}
acsacl.pl
}
authenticate {
}
}
Используются только два модуля - acsacl.pl и load.dacl (можно добавить логирование). В общем из имен и так понятно: первый - это наш модуль обработки dACL, второй - загрузка пользовательского ACL на основе файла конфигурации. Загрузка группового ACL производится непосредственно в описании virtual-server (ибо, как упоминалось ранее, грузить его из файла users - ну его нафиг). Добавим их определения в конфигурацию сервера:
- определяем инстанс perl-модуля, с именем acsacl.pl (это имя используется для указания в virtual_server); единственная определяемая функция func_authorize = acsacl; и конфигурационные параметры модуля ACL-attr и ACL-chunksize - в модуле используются как ключи хэша %RAD_PERLCONF:
#/etc/raddb/mods-enabled/acsacl.pl
perl acsacl.pl {
filename = ${modconfdir}/perl/custom-module.pl
func_authorize = acsacl
config {
ACL-attr = "Cisco-AVPair"
ACL-chunksize = 4000
}
}
- определяем инстанс file-модуля, с именем load.dacl (это имя так-же используется для указания в virtual_server); единственный параметр модуля - имя файла с acl в формате users:
#/etc/raddb/mods-enabled/load.dacl
files load.dacl {
filename = ${modconfdir}/${.:name}/${.:instance}/dacl
}
- ну и файл с пользовательским ACL:
#/etc/raddb/mods-config/files/load.dacl/dacl
# пользовательский ACL
username@DACL Hint == "ACSACL", Cisco-AVPair += "ip:inacl#1=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 3389"
И в итоге получаем:
Sent Access-Challenge Id 122 from server to client
Cisco-AVPair += "ip:inacl#1=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 3389"
Cisco-AVPair += "ip:inacl#10=permit tcp 10.0.0.0 255.0.0.0 10.0.0.0 255.0.0.0 eq 3389"
Cisco-AVPair += "ip:inacl#11=permit udp 10.0.0.0 255.0.0.0 any eq 53"
...
Cisco-AVPair += "ip:inacl#45=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 443"
State = 0x3435
Received Access-Request Id 123 from client to server
User-Name = "#ACSACL#-IP-username-1556032425"
User-Password = "password"
State = 0x3435
Cisco-AVPair = "aaa:service=vpn"
Cisco-AVPair = "aaa:event=acl-download"
Message-Authenticator = 0x40eeae8eccb5fa8895fbc4068f041575
Sent Access-Accept Id 123 from server to client
Cisco-AVPair += "ip:inacl#46=10.0.0.0 255.0.0.0 any eq 443"
...
Cisco-AVPair += "ip:inacl#57=deny ip any any"
Received Accounting-Request Id 124 from client to server
User-Name = "username"
Acct-Status-Type = Start
Acct-Delay-Time = 0
Acct-Session-Id = "CEA00097"
Acct-Authentic = RADIUS
__
Собственно всё. Дальше можно только уточнить упомянутое "нафига?"...
-
Cisco ACS/ISE имеет ограниченную возможность профилирования авторизации пользователя (в том, что касается dACL) - допускается собственно указание dACL в Authorization Profile, и дополнительный cavp-acl индивидуально для пользователя (с вышеупомянутым нюансом для FTD). Authorization Profile применяется условно - в зависимости от набора критериев применяется тот или иной AP. Ни возможности консолидации ACL разных групп, ни тем более возможности загрузки ACL из внешних источников там нет. В этом смысле возможности ACS/ISE не изменились со времен ACS 3.0 for Windows (EoS/EoL начала века, если что). Хотя функционал в принципе востребованный - на community.cisco.com мне попадался вопрос спеца, мигрирующего с Juniper на Cisco, о возможности консолидации ACL, назначенных разным AD группам. По его замечаниям получалось, что в решениях Juniper такая возможность есть (но утверждать не берусь - с Juniper вообще никогда не сталкивался).
Такие ограничения - это не хорошо и не плохо, просто диктуют методику построения политик доступа, исходя из возможностей продуктов вендора (что в принципе характерно для проприетарных решений: "делайте как дозволено/рекомендовано, и не выделывайтесь"); к тому же предъявляются довольно жесткие требования к составу и поколению оборудования (только наше и помощнее/поновее). Тем более, что по сравнению с ACS, ISE обладает гораздо большим функционалом по профилированию уже не только доступа пользователя на уровне периметра, а пользовательского трафика в масштабах сети в целом - Cisco TrustSec и STG. Но опять же - помощнее/поновее и как водится - подороже, а экономия IT бюджета принимает порой самые причудливые формы...
FreeRADIUS с точностью до наоборот - не обладая изысканным функционалом из коробки, предоставляет неограниченные (ну кроме ограничений оборудования вендора :-) ) возможности при построении политик доступа. Любая задача, которую можно формализовать, решаема в принципе - было бы внятное описание технологии от вендора (собственно, пример выше). Например вопрос поддержки merge-dacl на ASA/FTD решается просто делегированием этого функционала на сервер - можно объединять произвольное количество ACL на этапе формирования dACL на сервере - как групповых, так и индивидуальных, а клиенту отдавать уже готовый конечный ACL. Даже профилирование по пользовательским OS/Device доступно прямо как в ISE - если отбросить маркетинговые заявления Cisco, это функционал FTD/FMC, а не ISE (на ней только интерфейс настройки политик и предварительно заполненная база значений):
Received Access-Request Id 189 from client to server User-Name = "username" ... Cisco-AVPair = "mdm-tlv=device-platform=win" Cisco-AVPair = "mdm-tlv=device-platform-version=6.3.9600 " Cisco-AVPair = "mdm-tlv=ac-user-agent=AnyConnect Windows 4.6.03049" Cisco-AVPair = "mdm-tlv=device-type=VMware, Inc. VMware Virtual Platform"
-
Не рассмотренным остался специфичный функционал DAE/CoA - RFC 5176 и RFC 8559, который так же можно прикрутить к связке FreeRADIUS/ASA(FTD) и получить совсем уж монструозное поделие. Вкратце - это, без преувеличения, долгожданное расширение протокола RADIUS, позволяющее менять параметры авторизации активной сессии по инициативе сервера (о терминологии ниже). Возможности применения требуют отдельного, и довольно глубокого, рассмотрения (примеры возможных сценариев - ASA Version 9.2.1 VPN Posture with ISE Configuration Example и ISE and FirePower integration - remediation service example). На стороне клиента доступно начиная с версий ASA 9.2 и FTD 6.3.
Вообще-то эта фенька называется именно DAE (Dynamic Authorization Extensions), и состоит из двух видов сообщений - Disconnect Messages (DMs) и Change-of-Authorization (CoA) Messages. DMs предназначены для завершения/обновления сессии, так что основной функционал несет собственно CoA. Поэтому технологию частенько (в т.ч. в соответствующих RFC, документации FreeRADIUS и далее по тексту) называют именно CoA.В контексте статьи, данный функционал позволяет отлаживать процедуру загрузки dACL без реаутентификации пользователя:
- аутентификация клиента, поддерживающего CoA, сопровождается атрибутами audit-session-id= и coa-push=true:
Received Access-Request Id 129 from client to server User-Name = "username" User-Password = "password" Cisco-AVPair = "audit-session-id=c0a800010009b0005cc6a1ee" Cisco-AVPair = "coa-push=true" Sent Access-Accept Id 129 from server to client Cisco-AVPair += "ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-username-1556035772"
- сервер может ответить вообще без параметров авторизации, или с именем dACL. В данном случае это не важно - как мы увидим далее, параметры авторизации, переданные в CoA запросе сервера, заменят аналогичные параметры текущей сессии:
#client CLI: > show vpn-sessiondb detail anyconnect filter name username Session Type: AnyConnect Detailed Username : username Index : 155 ... Audt Sess ID : c0a800010009b0005cc6a1ee ... DTLS-Tunnel: Tunnel ID : 155.3 ... Filter Name : #ACSACL#-IP-username-1556035772
В общем случае, для идентификации сессии могут использоваться атрибуты, однозначно её (сессию) идентифицирующие (впрочем стандарт допускает и CoA для нескольких сессий). Например User-Name или комбинация Calling-Station-Id и Acct-Session-Id - это вопрос реализации связки клиент/сервер. Cisco использует VSA audit-session-id. Т.е. для процедуры авторизации стандартом вводится отдельное понятие "Session identification attributes", поскольку одним User-Name здесь уже не обойтись.
В RFC 5176 и в примерах конфигурации FreeRADIUS используется порт UDP/3799. Причем в RFC, как водится (видимо дабы оставить простор для дискуссий) номер порта указывается исключительно в контексте, но очень императивно: "The Disconnect-Request packet is sent to UDP port 3799, and identifies the NAS as well as the user session(s) to be terminated by inclusion of the identification attributes described in Section 3" (вот так вот - на порт 3799, и ни как иначе). Cisco по-умолчанию использует порт UDP/1700, но никто не мешает его сменить - там же, в настройках RADIUS группы, где необходимо включить "dynamic authorization". Важным нюансом является тот факт, что клиент теперь слушает порт/принимает запросы - т.е. выполняет роль сервера (CoA-Server или DAS (Dynamic Authorization Server)), а RADIUS сервер - роль клиента (CoA-Client или DAC (Dynamic Authorization Client)). И в терминологии, которая вроде уже устоялась для RADIUS протокола (клиент/сервер/пользователь), наступает вообще полная опа (особенно учитывая тот факт, что CoA-Client не обязан совпадать с исходным (аутентифицирующим) RADIUS сервером). Так что пусть и дальше остается клиентом - что бы он там ни слушал... :-)
- отправляем на клиент CoA запрос:
[root@server ~]# radclient -P udp -sx -r 1 client_ip:1700 coa secret << EOF > Cisco-AVPair+="audit-session-id=c0a800010009b0005cc6a1ee" > Cisco-AVPair+="ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-username-1556035773" > EOF Sent CoA-Request Id 190 from 0.0.0.0:39566 to client_ip:3799 length 135 Cisco-AVPair += "audit-session-id=c0a800010009b0005cc6a1ee" Cisco-AVPair += "ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-username-1556035773" Received CoA-ACK Id 190 from client_ip:3799 to 0.0.0.0:0 length 20 Packet summary: Accepted : 1 Rejected : 0 Lost : 0 Passed filter : 1 Failed filter : 0
- и получаем уже знакомый нам запрос на загрузку dACL от клиента:
Received Access-Request Id 130 from client to server User-Name = "#ACSACL#-IP-username-1556035773" Cisco-AVPair = "audit-session-id=c0a800010009b0005cc6a1ee" Cisco-AVPair = "aaa:service=vpn" Cisco-AVPair = "aaa:event=acl-download" Message-Authenticator = 0xeb9cc108453a32e495519cd606d1664b Cisco-AVPair = "coa-push=true" Sent Access-Challenge Id 130 from server to client Cisco-AVPair += "ip:inacl#1=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 3389" Cisco-AVPair += "ip:inacl#10=permit tcp 10.0.0.0 255.0.0.0 10.0.0.0 255.0.0.0 eq 3389" Cisco-AVPair += "ip:inacl#11=permit udp 10.0.0.0 255.0.0.0 any eq 53" ... Cisco-AVPair += "ip:inacl#45=permit tcp 10.0.0.0 255.0.0.0 172.16.0.0 255.240.0.0 eq 443" State = 0x3435 Received Access-Request Id 131 from client to server User-Name = "#ACSACL#-IP-username-1556035773" Cisco-AVPair = "audit-session-id=c0a800010009b0005cc6a1ee" State = 0x3435 Cisco-AVPair = "aaa:service=vpn" Cisco-AVPair = "aaa:event=acl-download" Message-Authenticator = 0x9e147553558c8e48f4a295474f645b49 Cisco-AVPair = "coa-push=true" Sent Access-Accept Id 132 from server to client Cisco-AVPair += "ip:inacl#46=10.0.0.0 255.0.0.0 any eq 443" ... Cisco-AVPair += "ip:inacl#57=deny ip any any"
- убеждаемся, что параметры авторизации заменены для той-же сессии:
#client CLI: > show vpn-sessiondb detail anyconnect filter name username Session Type: AnyConnect Detailed Username : username Index : 155 ... Audt Sess ID : c0a800010009b0005cc6a1ee ... DTLS-Tunnel: Tunnel ID : 155.3 ... Filter Name : #ACSACL#-IP-username-1556035773
и удовлетворенно почесываем ЧСВ :-) Кастомный CoA-Server для OCServ потыкаем палочкой в отдельном трактате.