27 апреля 2024
27 апреля 2024
17 апреля 2024 18 апреля 2024 Linux docker stunnel4
Серверную часть тонеля stunnel4 не имеет смысла описывать, а вот создания контейнера шифрующего трафика ещё ни где особо не описано. Итак
В директори services/stunnel4 размещаем 3 файла
в файле stunnel.conf пишем примерно такое
client = yes
pid = /var/run/stunnel.pid
foreground = yes
[postgres]
accept = 0.0.0.0:55432
connect = [remote-ip]:15432
cert = /etc/stunnel/[remote-ip].pem
Разумеется [remote-ip] заменяем на свой
В файле Dockerfile пишем вот такое
from avccvut/stunnel4:0.1-5.44
COPY ./[remote-ip].pem /etc/stunnel/
COPY ./stunnel.conf /etc/stunnel/
CMD ["stunnel4", "/etc/stunnel/stunnel.conf"]
В файле docker-compose.yml добавляем новый такой контейнер:
postgres:
# ...
stunnel4:
container_name: ${APP_NAME}-stunnel4
build:
context: services/stunnel4
ports:
- 55432:55432
После того как контейнер будет запущен обращаться к продакшен базе вот так
docker-compose exec postgres psql postgresql://user:psswrd@stunnel4:55432/dbname
Или например вот так можно скорпировать таблицу из внешней базы в локальный контейнер
docker-compose exec postgres pg_dump postgresql://user:psswrd@stunnel4:55432/dbname -t auth_user | docker-compose exec postgres psql dbname
18 декабря 2023 СуБД fail2ban postgres
Если postgres используется в качестве мастерсервера для репликаций то у него должен быть открыт порт для подключения клиентов извне, соответственно открытый порт начнут сканировать с целью подобрать пароли и тогда в логах появится вот такое записи
postgres@postgres FATAL: no pg_hba.conf entry for host "115.216.124.164", user "postgres", database "postgres", no encryption
postgres@postgres FATAL: no pg_hba.conf entry for host "222.90.83.209", user "postgres", database "postgres", no encryption
Причём, подбор паролей идёт активно, десятки тысяч запросов за сутки
for f in /var/log/postgresql/postgresql-14-main.log.*.gz; do
echo `date -r $f +"%Y-%m-%d"` `zcat $f | grep 'no pg_hba.conf entry for host' |wc -l` ;
done
2023-10-15 79090
2023-12-10 81664
2023-12-03 33115
2023-11-26 22769
2023-11-19 100753
2023-11-12 70794
2023-11-05 130725
2023-10-29 82514
2023-10-22 119528
Для блокировки адресов источников запроса необходимо добавить фильтр и добавить новое правило в fail2ban
/etc/fail2ban/filter.d/postgres.conf
[Definition]
failregex = FATAL: no pg_hba.conf entry for host "<HOST>"
Затем созданное имя фильтра необходимо использовать в правиле, добавив в конец файла /etc/fail2ban/jail.conf
[postgresql]
enabled = true
filter = postgres
action = iptables[name=PostgreSQL, port=5432, protocol=tcp, blocktype=DROP]
sendmail-whois[name=PostgreSQL, dest=root]
logpath = /var/log/postgresql/postgresql-14-main.log
maxretry = 3
findtime = 600
bantime = 604800
После этого необходимо перезапустить fail2ban: service fail2ban restart
и проконтролировать перезапуск сервиса просмотрел логи: tail -f /var/log/fail2ban.log
19 октября 2023 Rust
use std::io::{self, Write};
fn main() -> io::Result<()> {
for (idx, line) in std::io::stdin().lines().enumerate() {
match line {
Ok(_line) => {
let _cnt = _line.len();
let _line = format!("{idx:>5}:[{_cnt:>3}] -> {_line}\n");
let _ = io::stdout().write(_line.as_bytes());
},
Err(e) => println!("Error: {e}")
};
}
Ok(())
}
Здесь интересным может показаться разворачивание итератора line с помощью конструкции match, так как в line может быть Ok("строка") или Err("ошибка"), то для использования необходимо развернуть
А так же вместо println используется запись в stdout
19 октября 2023 Rust stdin lines enumerate
use std::io;
fn main() -> io::Result<()> {
for (idx, line) in std::io::stdin().lines().enumerate() {
println!("Номер строки: {idx:>5} -> {}", line.unwrap() );
}
Ok(())
}
тут в цикле перебираются пронумерованные строки переданные на входящий канал
{idx:5}, указывает формированить вывод строк в блоке из 5 символов
cat src/main.rs | cargo r
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/read_stdin`
Номер строки: 0 -> use std::io;
Номер строки: 1 ->
Номер строки: 2 ->
Номер строки: 3 ->
Номер строки: 4 -> fn main() -> io::Result<()> {
Номер строки: 5 ->
Номер строки: 6 -> for (idx, line) in std::io::stdin().lines().enumerate() {
Номер строки: 7 -> println!("Номер строки: {idx:>5} -> {}", line.unwrap() );
Номер строки: 8 -> }
Номер строки: 9 ->
Номер строки: 10 -> Ok(())
Номер строки: 11 -> }
Тоже самое но на python
#!/usr/bin/env python
import sys
for idx, line in enumerate(sys.stdin.readlines()):
print(f"Номер строки: {idx:>5} -> {line}", end='')
15 октября 2023 Bash curl awk sort uniq egrep white read
Если необходимо проверить большое количество ссылок на сайте то можно воспользоваться вот таким bash скрипт
site=http://127.0.0.1:8080
curl $site 2> /dev/null | egrep -o 'href="[^"]+"' | awk -F\" '{print $2}' | grep /catalog/ | sort | uniq | while read u; do
curl $site$u -o /dev/null -w "%{http_code} $u\n" -s
done >> check-menu.txt
Данный скрипт
13 октября 2023 Bash awk while read
Имеет большое количество файлов которые могут повторяться по содержимому и стоит задача оставить только уникальные файлы по содержимому. Для определения уникальности будем использовать контрольную сумму md5, а для автоматизации bash скрипт
#!/bin/bash
echo Чистка от дублей
x=""
ls /tmp/base_1s-202* |sort | while read f; do
_x=$(md5sum $f|awk '{print $1}')
if [ $_x == "$x" ]; then
echo удаление $f $_x
rm $f
else
x=$_x
echo сохранение $_x $f
fi
done
11 октября 2023 13 октября 2023 Bash for read while git ls-files
Обычно я использую для обработки больлих списков следующую конструкцию
for x in $(git ls-files); do
du -sm "$x"
done
здесть создаётся локальная переменная в которой размещается результат операции find
Более оптимальная конструкция без использования локальной переменной через цикл while и read
git ls-files | while read x;
do du -sm "$x";
done
11 октября 2023 13 октября 2023 Python url parse dict reduce
Однострочник для преобразования строки URL запроса в словь с данными ключ=значение
reduce(lambda a, x: {**a, **dict([x.split('=')])}, b.split('&'), dict())
Здесь три составные части, начнём с конца
dict() - тут объявляется пустой словарь которые в будет пополняться результатами вычислений
request_body.split('&') - здесь тело строки запроса делится на лексемы состоящие из пары ключе/значение разделённых сиволом '='
lambda a, x: {**dict([x.split('=')]), **a} - здесь каждая лексема дробится по заделительному символу '=' добавляется в словарь соззданный в начале и возвращается результатом
у этого способа есть явный минус, он исключает дубирование пар ключей в теле запроса так как в славе ключи всегда уникальны
09 октября 2023 Linux ubuntu 22.04 haproxy rsyslog logging
Недавно обнаружил, что современные на 2023 года Ubuntu не логируют работу Haproxy. Оказывается в них отсутствует и не настроен rsyslog
проверить можно вот так
apt-cache policy rsyslog
установить вот так
sudo apt-get install rsyslog
Затем необходимо настроить его для работы на 514 пору, для этого необходимо раскоментировать следующие настройки
egrep 'im(tcp|udp)' /etc/rsyslog.conf
module(load="imudp")
input(type="imudp" port="514")
module(load="imtcp")
input(type="imtcp" port="514")
и перезапустить
service rsyslog restart
Затем проверить в /etc/haproxy/haproxy.cfg глобальную настройку
global
log 127.0.0.1:514 local0
defaults
log global
и перезапустить haproxy
haproxy -c -V -f /etc/haproxy/haproxy.cfg && service haproxy reload
А так же подумать на счёт переноса haproxy в отдельный контейнер
04 октября 2023 Linux docker ftp proFTPD
Современный backend может не содержать на физическом сервере своих привычных файлов, там может даже не было СУБД и Веб-сервера потому что вся система размещается в контейнерах Docker
Для того чтобы предоставить доступ к файлам в таком контейнере можно воспользовать образом proFTPD и включить его в конфигурацию docker-compose
Ниже представлена настройка такого контейнера
ftp:
image: instantlinux/proftpd
container_name: ${APP_NAME}-ftp
ports:
- "2100:21"
- "30091-30100:30091-30100"
env_file: .env
volumes:
- ./ftp/secrets:/run/secrets
- ./site:/home/site/
environment:
- PASV_ADDRESS=0.0.0.0
- ANONYMOUS_DISABLE=on
- TZ=Europe/Moscow
- SFTP_ENABLE=off
- PASV_MAX_PORT=30100
- PASV_MIN_PORT=30091
так же предполагается наличие .env файла в котором необходимо определить следующие переменные
FTPUSER_UID=1000
FTPUSER_NAME=ftp_user
FTPUSER_PASSWORD_SECRET=Yhatztna7%$4A8hag
Переменная FTPUSER_UID должна быть равна ID текущего пользовать от имени которого запускается контейнер
Переменные FTPUSER_NAME и FTPUSER_PASSWORD_SECRET ипользуется для генерации пароля и поиска этого пароля в специальном файле в директории /run/secrets
Дело в том, что proFTPD использует пароли в определённом формате, для нормальной работы необходимо сгенерировать пароль и положить в специальный файл, который будет подмотирован в образ proFTPD
python3 -c "import crypt,random,string; print(crypt.crypt('$FTPUSER_PASSWORD_SECRET', '\$6\$' + ''.join( [random.choice(string.ascii_letters + string.digits) for _ in range(16)])))" > ftp/secrets/$FTPUSER_PASSWORD_SECRET
в результате получится вот такой файл с паролем
cat ftp/secrets/Yhatztna7%\$4A8hag
$6$XyHVN6aqgQvgj7Vv$Ac/9hKk0WYOmYPPh/hcG/yLvMAAgi91.k5lC2U4Dx/1PEe0KtW8NsLOhN6GBzcX8TKQPF51JHmyBX580pZ9.A0
Если хочется дополнительных опцией автозапуска то достаточно изучить файл инициализации контейнера
docker-compose exec ftp cat /usr/local/bin/entrypoint.sh
29 сентября 2023 СуБД Postgres RECURSIVE
Рекурсивных запрос к Postgres состоит из двух, объединённых запросов
Рекурсия начинает с добавляния в результат данных от первой выборки и будет продолждать до тех пока второй запрос будет возвращать данные
WITH RECURSIVE family_children as (
select id, 0 as level, fio from family where fio ~ '^Иванов'
union
select family.id, level+1 as level, family.fio from family join parent on family.id=parent.child join family on family.id=parent.id
) select * from family_children;
Приведённы в примере запрос начинается с поиска в Таблице Имён записей всех Ивановых. В результат подмешивается переменная level, level определяет уровень родства
Затем запрос продолжает поиском записей родителей любых Ивановых. При этом уровень родства увеличился единицу
Запросы будут продолжаться пока будут находиться родители для предыдущей порции детей
29 августа 2023 Bash awk haproxy
Задача, выделить из логов haproxy статусы запросов, бакенды обработчиков и урлы запросов, так же выводить количество счётчик статусов запросов
tail -f /var/log/haproxy-traffic.log |awk -F' ' '{if (NF == 20) print $11,'\t', ++count[$11],'\t', $9, $19 }'
Программа awk состоит из условия и инкремента счётчика
if (NF == 20) print $11,'\t', ++count[$11],'\t', $9, $19 }
здесь отбираются строки состоящие из 20 слов, выводится status_code запроса, а так же результат инкремента счётчика статусов
подобным образом можно организовать подсчёт количества обращений к бакенду и ссылкам
if (NF == 20) print $11,'\t', ++count[$11],'\t',++count[$19],'\t', $9, ++count[$19],'\t', $19 }
28 августа 2023 Python Docker Django
Задача, извлеч значение настроек запущенного контейнера Django.
Например, необходимо сбросить локальный кешь Django, настройки этого кеша находятся в settings.py файле в словаре
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache-{}".format(SITE_PREFIX),
"TIMEOUT": 360,
"OPTIONS": {
"MAX_ENTRIES": 1000
}
},
"redis": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_LOCATION,
"OPTIONS": {
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
}
},
}
В данном случае необходимо извлеч значение CACHES -> default -> LOCATION
Если Django запущено в контейнере то извлечь это значение можно вот таким образом
docker-compose exec -e DJANGO_SETTINGS_MODULE=centrsvet.settings django poetry run python -c 'from django.conf import settings;print(settings.CACHES["default"]["LOCATION"])'
/var/tmp/django_cache-T1
Представленная команда состоит из целого ряда компонентов
08 августа 2023 Python Django Form Validator custom
Если необходимо выделить поле формы содержащее ошибку то необходимо переопредить форму так
class FeedbackForm(forms.Form):
name = forms.CharField(label="Имя", max_length=50)
email = forms.EmailField(label="E-mail", max_length=50)
phone = forms.CharField(label="Телефон", max_length=20)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for visible in self.visible_fields():
if visible.errors: # при наличии ошибки поля, добавляем css-класс и меняем title
visible.field.widget.attrs["class"] = "form__input has-error"
visible.field.widget.attrs["title"] = "".join(visible.errors)
else:
visible.field.widget.attrs["class"] = "form__input"
visible.field.widget.attrs["required"] = True
def clean_email(self):
return check_email(self.cleaned_data["email"]).lower()
def clean_name(self):
return self.cleaned_data["name"].title()
def clean_phone(self):
_phone = self.cleaned_data["phone"]
_phone = "".join(re.findall("([\\d]+)", _phone))
if re.match( "^\\+?[1-9][0-9]{7,14}$", _phone):
return f"+{_phone}"
raise ValidationError("Телефон не соответствует формату")
Конструктор формы позволяет переопредить настройки виджета поля формы, а так же предоставляет список ошибок обрануженных при валидации формы