Доброго времени суток. В данной заметке хочу рассказать о простой аутентификации с помощь nginx и lua-скриптов.
Подняв у себя домашний сервер на ubuntu с plex и transmission и обзаведясь доменом, через который вывел это добро в большой мир, понял Я, что было бы неплохо обзавестись единой точкой аутентификации. Тем более nginx у меня уже был установлен (даже nginx-extras, что немаловажно, поскольку там есть lua).
Собравшись с мыслями, сформулировал требования:
- Отсутствие необходимости установки дополнительного ПО
- Отдельная страница аутентификации
- Сквозная аутентификация для всех сервисов за nginx
- Хотя бы минимальная защита от перебора
Вариант с nginx basic auth не устроил по причине отсутствия защиты от перебора, вариант с nginx auth PAM вызвал у меня недоверие по причине аутентификации по логину/паролю ОС. И оба варианта не дают возможности аутентификации через свою отдельную форму.
Алгоритм аутентификации довольно прост:
Ну что ж, приступим.
Для начала создадим lua-скрипт с некоторым функциями, которые понадобятся нам в дальнейшем:
/etc/nginx/lua/secure.lua
-- Количество попыток для ip/32 и User-Agent
local ip_ua_max = 10
-- Количество попыток для ip/32
local ip_4_max = 50
-- Количество попыток для ip/16
local ip_3_max = 100
-- Количество попыток для ip/8
local ip_2_max = 500
-- Количество попыток для ip/0
local ip_1_max = 1000
counters = {}
counters["ip_ua"] = {}
counters["ip_4"] = {}
counters["ip_3"] = {}
counters["ip_2"] = {}
counters["ip_1"] = {}
-- Проверка числа попыток (is_cnt=false) и учёт неуспешной попытки (is_cnt=true)
function is_secure(ip, user_agent, is_cnt)
local md5_ip_ua = ngx.md5(ip..user_agent)
local md5_ip_4 = ngx.md5(ip)
local md5_ip_3 = ""
local md5_ip_2 = ""
local md5_ip_1 = ""
local cnt = 0
for i in string.gmatch(ip, "%d+") do
cnt = cnt + 1
if cnt < 4 then
md5_ip_3 = md5_ip_3.."."..i
end
if cnt < 3 then
md5_ip_2 = md5_ip_2.."."..i
end
if cnt < 2 then
md5_ip_1 = md5_ip_1.."."..i
end
end
md5_ip_3 = ngx.md5(md5_ip_3)
md5_ip_2 = ngx.md5(md5_ip_2)
md5_ip_1 = ngx.md5(md5_ip_1)
if is_cnt then
-- Учитываем неуспешную попытку
counters["ip_ua"][md5_ip_ua] = (counters["ip_ua"][md5_ip_ua] or 0) + 1
counters["ip_4"][md5_ip_4] = (counters["ip_4"][md5_ip_4] or 0) + 1
counters["ip_3"][md5_ip_3] = (counters["ip_3"][md5_ip_3] or 0) + 1
counters["ip_2"][md5_ip_2] = (counters["ip_2"][md5_ip_2] or 0) + 1
counters["ip_1"][md5_ip_1] = (counters["ip_1"][md5_ip_1] or 0) + 1
-- Пишем в лог подробности неуспешной попытки
log_file = io.open("/var/log/nginx/access.log", "a")
log_file:write(ip.." "..(counters["ip_ua"][md5_ip_ua] or 0).." "..(counters["ip_4"][md5_ip_4] or 0).." "..(counters["ip_3"][md5_ip_3] or 0).." "..(counters["ip_2"][md5_ip_2] or 0).." "..(counters["ip_1"][md5_ip_1] or 0).." "..user_agent.."\n")
log_file:close()
else
-- Проверяем число неуспешных попыток
if
(counters["ip_ua"][md5_ip_ua] or 0) > ip_ua_max or
(counters["ip_4"][md5_ip_4] or 0) > ip_4_max or
(counters["ip_3"][md5_ip_3] or 0) > ip_3_max or
(counters["ip_2"][md5_ip_2] or 0) > ip_2_max or
(counters["ip_1"][md5_ip_1] or 0) > ip_1_max
then
return false
else
return true
end
end
end
-- Проверка логина/пароля
-- В данном примере просто сравнение с хэшом из файла, при желании в данной функции можно реализовать проверку логина/пароля где угодно (в БД например)
function sing_in(log, pass)
local auth_file = io.open("/etc/nginx/auth/pass","r")
for line in io.lines("/etc/nginx/auth/pass") do
if line == log..":"..ngx.md5(pass) then
auth_file:close()
return true
end
end
auth_file:close()
return false
end
-- Сохраняем функции в глобальном контейнере secure
local secure = ngx.shared.secure
secure:set("sing_in", sing_in)
secure:set("is_secure", is_secure)
Добавим инициализацию данного скрипта в глобальный конфиг nginx:
/etc/nginx/nginx.conf
• • •
http {
• • •
# Объявляем глобальный контейнер
lua_shared_dict secure 10m;
# Инициализируем скрипт
init_by_lua_file /etc/nginx/lua/secure.lua;
• • •
include /etc/nginx/conf.d/*.conf;
}
Теперь создадим lua-скрипт для проверки cookie (шаги 2, 2.1, 3):
/etc/nginx/lua/access.lua
-- Адрес страницы аутентификации
local req_url_err = "https://auth.somedomain.ru"
-- Получаем User-Agent адрес клиента
local ua = ngx.req.get_headers()["User-Agent"]
-- Получаем токен и время из cookie
local auth_str = ngx.var.cookie_sv_auth
local auth_token = ""
local life_time = ""
if auth_str ~= nil and auth_str:find("|") ~= nil then
local divider = auth_str:find("|")
auth_token = auth_str:sub(0,divider-1)
life_time = auth_str:sub(divider+1)
-- 2. Проверяем валидность токена
if auth_token == ngx.encode_base64(ngx.hmac_sha1("ОЧЕНЬ_СЕКРЕТНАЯ_ОЧЕНЬ_ДЛИННАЯ_СТРОКА_НАПРИМЕР_КАКОЙ-НИБУДЬ_32-УХЗНАЧНЫЙ_ХЭШ",ua.."|"..life_time)) and tonumber(life_time) >= ngx.time() then
-- Токен валиден
return
end
end
-- Токен не валиден или отсутствует
-- 2.1. Сохраняем в coockie url назначения
ngx.header["Set-Cookie"] = "sv_req_url="..ngx.req.get_headers()["Host"].."; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()+60*60).."; Secure; HttpOnly"
-- И возвращаем редирект на страницу аутентификации
return ngx.redirect(req_url_err)
Добавим проверку данным скриптом в конфиги внутренних сервисов:
/etc/nginx/conf.d/plex.conf
server {
listen 443 ssl;
server_name plex.somedomain.ru;
access_by_lua_file /etc/nginx/lua/access.lua;
location / {
proxy_pass http://localhost:32400;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
ssl on;
• • •
}
Создадим страницу аутентификации:
/var/www/html/auth.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>somedomain</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{
height: 100%;
background-color: rgb(64, 64, 64);
text-align:center;
align:center;
vertical-align: middle;
}
form {
display: inline-block;
text-align: center;
vertical-align: middle;
position:absolute;
top:50%;
right:0;
left:0;
}
input{
color: rgb(0, 255, 0);
text-align: center;
border: 2px solid;
border-color: rgb(0, 255, 0);
background-color: rgb(64, 64, 64);
}
::-webkit-input-placeholder{
color:rgb(0, 255, 0);
text-align: center;
}
::-moz-placeholder{
color:rgb(0, 255, 0);
text-align: center;
}
:-moz-placeholder{
color:rgb(0, 255, 0);
text-align: center;
}
:-ms-input-placeholder{
color:rgb(0, 255, 0);
text-align: center;
}
br{
display: block;
margin: 7px 0;
line-height: 7px;
content: " ";
}
</style>
</head>
<body>
<form method="post">
<input type="text" name="login" placeholder="login" autocomplete="off">
<br>
<input type="password" name="password" placeholder="password" autocomplete="off">
<br>
<input type="submit" value="sign in">
</form>
</body>
</html>
И добавим для неё конфиг nginx:
/etc/nginx/conf.d/auth.conf
server {
listen 443 ssl;
server_name auth.somedomain.ru;
access_by_lua_file /etc/nginx/lua/auth_access.lua;
location / {
default_type 'text/html';
root /var/www/html/;
index auth.html;
if ($request_method = POST ) {
content_by_lua_file /etc/nginx/lua/auth.lua;
}
}
ssl on;
• • •
}
В данном конфиге делаем проверку числа попыток аутентификации с помощью «auth_access.lua» (шаг 4, 4.2)
/etc/nginx/lua/auth_access.lua
-- Берём из глобального контейнера secure нужные нам функции
local secure = ngx.shared.secure
is_secure = secure:get("is_secure")
-- Получаем ip адрес клиента
local ip = ngx.var.remote_addr
-- Получаем User-Agent адрес клиента
local ua = ngx.req.get_headers()["User-Agent"]
-- 4. Проверка количества попыток аутентификации
if is_secure(ip,ua,false) then
-- Проверка пройдена, удаляем невалидный токен
ngx.header["Set-Cookie"] = {"sv_auth=; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()-60).."; Secure; HttpOnly"}
return
end
-- 4.2. Проверка не пройдена, возвращаем HTTP 403
ngx.exit(ngx.HTTP_FORBIDDEN)
И проверку логина/пароля с помощью «auth.lua» (шаг 5, 5.1, 2.2)
/etc/nginx/lua/auth.lua
-- Берём из глобального контейнера secure нужные нам функции
local secure = ngx.shared.secure
sing_in = secure:get("sing_in")
is_secure = secure:get("is_secure")
-- Получаем ip адрес клиента
local ip = ngx.var.remote_addr
-- Получаем User-Agent адрес клиента
local ua = ngx.req.get_headers()["User-Agent"]
-- Адрес страницы аутентификации
local req_url_err = "https://auth.somedomain.ru"
-- Адрес назначения из cookie или дефолтный адрес, если в cookie адреса нет
local req_url = "https://"..(ngx.var.cookie_sv_req_url or "somedomain.ru")
-- Проверяем наличие параметров POST-запроса
ngx.req.read_body()
local args, err = ngx.req.get_post_args()
if args then
-- 4.1. Читаем из POST-запроса логин и пароль
local log
local pass
for key, val in pairs(args) do
if key == "login" then
log = val
elseif key == "password" then
pass = val
end
end
-- Проверяем, что логин и пароль не пустые
if log ~= nil and pass ~= nil then
-- 5. Проверяем валидны ли логин и пароль
if sing_in(log, pass) then
-- Если валидны
-- Задаём время жизни токена (сутки)
local life_time = ngx.time()+86400
-- Генерируем токен
local auth_str = ngx.encode_base64(ngx.hmac_sha1("ОЧЕНЬ_СЕКРЕТНАЯ_ОЧЕНЬ_ДЛИННАЯ_СТРОКА_НАПРИМЕР_КАКОЙ-НИБУДЬ_32-УХЗНАЧНЫЙ_ХЭШ",ua.."|"..life_time)).."|"..life_time
-- 5.1. Записываем токен в cookie и удаляем оттуда url назначения
ngx.header["Set-Cookie"] = {"sv_auth="..auth_str.."; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()+60*60*24).."; Secure; HttpOnly","sv_req_url="..ngx.req.get_headers()["Host"].."; path=/; domain=.somedomain.ru; Expires="..ngx.cookie_time(ngx.time()-60).."; Secure; HttpOnly"}
-- 2.2. Возвращаем редирект на страницу назначения
return ngx.redirect(req_url)
end
-- 5.2. Если логин/пароль невалидны, учитываем это в подсчёте неуспешных попыток аутентификации
is_secure(ip,ua,true)
end
end
-- 3. Если логин и пароль не переданы или невалидны, возвращаем редирект на страницу аутентификации
ngx.redirect(req_url_err)
Теперь создадим файл с логином и паролем:
md5="`echo -n "PASSWORD" | md5sum`";echo -e "LOGIN"":`sed 's/^\([^ ]\+\) .*$/\1/' <<< "$md5"`" > ~/pass; sudo mv ~/pass /etc/nginx/auth/pass; sudo chown nginx:nginx /etc/nginx/auth/pass
Подставив вместо «LOGIN» логин, а вместо «PASSWORD» пароль.
Вот и всё, аутентификации реализована.
При добавлении сервисов, достаточно будет в конфигах указывать проверку по «access.lua»:
access_by_lua_file /etc/nginx/lua/access.lua;
Спасибо за внимание.
UPD 26.03.2018 (спасибо YourChief):
— Убрана функция nvl, за ненадобностью
— md5 при генерации токена заменено на HMAC
— В токен добавлено время его жизни
— md5 и HMAC используются встроенные в nginx