Общие фишки
- Настройка HTTP-аутентификации в Nginx
- Настройка SSH-подключения по ключам
- Разрешение нескольких RDP-подключений на Windows
- Копирование данных с помощью RSync
- Maintenance-мод для Nginx
- Быстрая настройка времени на Linux-хосте
- Установка WayDroid
- RDP на Linux (GNOME Remote Desktop)
- Как настроить Outlook 2019 для использования моего почтового сервера
- Настройка скрипта автозапуска через SystemD-юнит (серверный подход)
- Генерация нескольких сотен тестовых файлов в цикле (Linux)
- RU locale setup on Linux (debian-based only)
- Скрипт для генерации произвольной структуры файлов и папок
- Быстрая активация Windows и MS Office
- Выпуск и обновление сертификата Let's Encrypt (WildCard) через DNS
- Активация Windows и MS Office
- Настройка мультиязычности и поддержки кириллицы в Linux
Настройка HTTP-аутентификации в Nginx
Чтобы настроить веб-аутентификацию в Nginx нужно сделать всего пару вещей:
- Установить пакет apache2-utils
В Ubuntu это делается довольно просто:
apt-get install apache2-utils -y
- Создать файл с пользователями и паролями
# С указанием имени пользователя:
htpasswd -c /etc/nginx/.htpasswd username
Для безопасности, лучше сразу установить права и владельца для этого файла:
sudo chmod 640 /etc/nginx/.htpasswd
sudo chown root:www-data /etc/nginx/.htpasswd
- Настроить Nginx
server {
# ...
location /protected {
# Сама аутентификация: заголовок запроса пароля и файл с доступным УЗ
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;
# А дальше - произвольная конфигурация location
}
# ...
}
Обратите внимание: аутентификация работает только на конкретный location (и все, что за ним), а не на весь возможный пул
После всех манипуляций, нужно перезапустить Nginx:
nginx -t && nginx -s reload
При переходе на эндпоинт /protected (и все, что находится за ним), вам будет предложено ввести логин и пароль, который вы сгенерировали ранее
Опционально, вы можете отключить аутентификацию для каких-то вложенных путей:
# Здесь есть аутентификация
location / {
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;
# ...
}
# А эта часть - без аутентификации
location /public-page {
auth_basic off;
# ...
}
ВАЖНО!
HTTPS обязателен для Basic Auth, так как логин/пароль передаются в Base64. Любой, кто перехватит запрос, получит логин и пароль в открытом виде
Настройка SSH-подключения по ключам
Шаг 1. Генерация приватного и публичного ключа
Для генерации приватного и публичного ключа выполним команду:
ssh-keygen -a 1000 -b 4096 -o -t rsa
Далее необходимо:
1. Ввести путь, куда будут сохранены ключи – по-умолчанию это ~/.ssh/id_rsa
2. Ввести парольную фразу для расшифровки приватного ключа. Если оставить поле пустым, при его использовании пароль не будет запрашиваться
Так же важно добавить публичный ключ в один из конфигурационных файлов – authorized_keys:
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
Пояснение
SSH-ключ будет в файле id_rsa, в файле id_rsa.pub будет связанным с ним публичный ключ. Файл authorized_keys будет хранить в себе список из отпечатков (публичный ключей) для каждого приватного ключа, доступного для этого пользователя.
Шаг 2. Настройка SSHD
Изменим файл /etc/ssh/sshd_config, задав (изменив) ряд опций. Если закрытый ключ генерировался для любых пользователей, но root не должен иметь права входить на сервер:
# Запрещаем вход пользователю root
PermitRootLogin no
# Разрешаем использование ключей
PubkeyAuthentication yes
# Запрещаем использование паролей
PasswordAuthentication no
# Запрещаем аутентификацию пользователей через модуль PAM
# (встроенная аутентификация в Linux)
UsePAM no
Если закрытый ключ генерировался для пользователя root, изменяем всего 1 параметр:
# Разрешаем вход пользователю root
PermitRootLogin yes
По желанию, можно разрешить вход каким-то конкретным пользователям:
AllowUsers root
Шаг 3. Перезапуск демона SSH и проверка работоспособности
Для применения изменений выполним перезапуск сервера SSH:
systemctl restart ssh
Далее, попробуем войти на этот же сервер с нашим закрытым ключом:
ssh username@localhost -i /путь/к/приватному/ключу
Если ошибок в конфигурации нет, SSH предложит нам добавить хост в список доверенных (только при первом входе) и разрешит вход. Если на этапе задания пароля для приватного ключа вы установили пароль, он будет запрошен у вас для дешифровки.
Примечание
Возможно, понадобится отредактировать файлы в Include-директории. Обычно, это:
/etc/ssh/sshd_config.d/*.conf
В этих файлах хранятся настройки, которые могут перезаписать значения из основного конфигурационного файла SSHD, поэтому допустимо такое решение:
rm -f /etc/ssh/sshd_config.d/*.conf
ВАЖНО!
Эта команда удалит все содержимое с расширением *.conf из директории /etc/ssh/sshd_config.d !!!
Разрешение нескольких RDP-подключений на Windows
Зачем эта инструкция?
Удаленные пользователи могут подключаться к своим компьютерам Windows 10 или 11 через службу удаленных рабочих столов (RDP). Достаточно разрешить RDP доступ и подключиться к компьютеру с помощью любого клиента. Но есть ограничение на количество одновременных RDP сессии – допускается одновременная работа только одного удаленного пользователя. Если вы попробуете открыть вторую RDP сессию, появится предупреждение с запросом отключить сеанс первого пользователя.
Ограничение на количество одновременных RDP подключений в Windows является не техническим, но программным и лицензионным. Таким образом Microsoft запрещает создавать терминальный RDP сервер на базе рабочей станции для одновременной работы нескольких пользователей. Логика Microsoft проста: если вам нужен терминальный сервер — купите лицензию Windows Server, лицензии RDS CAL, установите и настройте роль Remote Desktop Session Host (RDSH).
Технически, любая редакция Windows при наличии достаточного количества оперативной памяти может обслуживать одновременную работу нескольких десятков удаленных пользователей. В среднем на одну RDP сессию пользователя без учета запускаемых приложений требуется 150-200 Мб памяти. Т.е. максимальное количество одновременных RDP сессий в теории ограничивается только ресурсами компьютера.
Решение проблемы
Мы будем использовать PowerShell скрипт для автоматического патчинга файла. Данный скрипт написан для версии Windows PowerShell, и не работает на PowerShell Core. Скрипт универсальный и может использоваться для внесения изменений в файл termsrv.dll на всех версиях Windows 10 и Windows 11.
# PowerShell скрипт модифицирует файл termsrv.dll, разрешая множественные RDP
# подключения к рабочим станциям на базе Windows 10 (1809 и выше) и Windows 11
# Подробности https://winitpro.ru/index.php/2015/09/02/neskolko-rdp-sessij-vwindows-10/
# Остановить службу, сделать копию файл и изменить разрешения
Stop-Service UmRdpService -Force
Stop-Service TermService -Force
$termsrv_dll_acl = Get-Acl c:\windows\system32\termsrv.dll
Copy-Item c:\windows\system32\termsrv.dll c:\windows\system32\termsrv.dll.copy
takeown /f c:\windows\system32\termsrv.dll
$new_termsrv_dll_owner = (Get-Acl c:\windows\system32\termsrv.dll).owner
cmd /c "icacls c:\windows\system32\termsrv.dll /Grant $($new_termsrv_dll_owner):F
/C"
# поиск шаблона в файле termsrv.dll
$dll_as_bytes = Get-Content c:\windows\system32\termsrv.dll -Raw -Encoding byte
$dll_as_text = $dll_as_bytes.forEach('ToString', 'X2') -join ' '
$patternregex = ([regex]'39 81 3C 06 00 00(\s\S\S){6}')
$patch = 'B8 00 01 00 00 89 81 38 06 00 00 90'
$checkPattern=Select-String -Pattern $patternregex -InputObject $dll_as_text
If ($checkPattern -ne $null) {
$dll_as_text_replaced = $dll_as_text -replace $patternregex, $patch
}
Elseif (Select-String -Pattern $patch -InputObject $dll_as_text) {
Write-Output 'The termsrv.dll file is already patch, exitting'
Exit
}
else {
Write-Output "Pattern not found "
}
# модификация файла termsrv.dll
[byte[]] $dll_as_bytes_replaced = -split $dll_as_text_replaced -replace '^', '0x'
Set-Content c:\windows\system32\termsrv.dll.patched -Encoding Byte -Value
$dll_as_bytes_replaced
# Сравним два файла
fc.exe /b c:\windows\system32\termsrv.dll.patched c:\windows\system32\termsrv.dll
# замена оригинального файла
Copy-Item c:\windows\system32\termsrv.dll.patched c:\windows\system32\termsrv.dll
-Force
Set-Acl c:\windows\system32\termsrv.dll $termsrv_dll_acl
Start-Service UmRdpService
Start-Service TermService
Чтобы выполнить скрипт, скачайте его на свой компьютер. Далее измените настройки политики запуска скриптов PowerShell:
Set-ExecutionPolicy Bypass -Scope Process -Force
Затем запускаем сам скрипт в PowerShell от имени администратора:
C:\users\root\desktop\rdp_patch.ps1
Скрипт нужно запускать после установки обновлений Windows, чтобы немедленно внести изменения в файл termsrv.dll (защищает вас от ручной правки файла после каждой установки обновлений). Скрипт подходит для всех версий Windows 10 старше 1809 и для Windows 11.
Преимущество этого способа включения нескольких RDP сессий в Windows в том, что он работает путем замены файла части файла оригинальной системной библиотеки termsrv.dll. Как следствие, на этот подход не реагируют антивирусы.
ВНИМАНИЕ!!! У ЭТОГО ПОДХОДА ЕСТЬ ОСОБЕННОСТЬ!
При каждом обновлении Windows 10 (или при обновлении версии файла termsrv.dll в рамках других обновлений) придется снова запускать скрипт, т.к. оригинальная библиотека вернется в свое прежнее состояние (до патча)!
Ссылки
- Оригинальная статья: https://web-profi.by/neskolko-odnovremennyx-rdp-sessij-polzovatelej-vwindows-10/
- Репозиторий проекта: https://github.com/winadm/posh/blob/master/Desktop/RDP_patch.ps1
Копирование данных с помощью RSync
Строение команды
Команда для копирования выглядит вот так:
rsync -aHAX --numeric-ids \
--info=progress2 --human-readable \
--partial --partial-dir=.rsync-partial --inplace --preallocate \
--compress --compress-choice=zstd --zl=3 \
/SOURCE/ user@DEST:/TARGET/
Пояснения к ключам:
--info=progress2 - отображает общий прогресс, скорость и ETA
--partial --partial-dir=.rsync-partial - позволяет докачивать большие файлы (например, 100 ГБ).
--inplace --preallocate - резервирование места и меньше фрагментации на приёмной стороне
-aHAX --numeric-ids - копирование прав, атрибутов, жёстких ссылок, ACL и xattrs (нужно sudo и одинаковые UID/GID)
--compress --compress-choice=zstd --zl=3 - сжатие через zstd (поддерживается в rsync ≥ 3.2). Если rsync старой версии, используйте просто -z
/SOURCE/ - путь к исходным данным (например: /var/www/nextcloud/data/)
user@DEST:/TARGET/ - путь к целевой папке на сервере и данные пользователя (пароль будет запрошен при запуске).
Если много уже сжатого контента (видео, архивы), использование --compress может замедлить процесс. В таком случае, уберите этот параметр.
Рекомендации по запуску
Чтобы копирование пережило обрыв SSH-сессии, используйте tmux:
Установка:
apt install tmux -y
Запуск:
tmux
Внутри сессии выполните команду rsync, которая приведена выше. Если SSH-сессия оборвётся, переподключитесь и получите список сессий:
tmux ls
Подключение к нужной сессии выполняется следующей командой:
tmux attach -t 0 # где 0 — номер сессии
Maintenance-мод для Nginx
Зачем?
Краткий ответ - за тем, чтобы при "отвале" сервиса, клиенты видели не дефолтный HTTP/500 ERROR, а вашу разметку.
Как?
В конфигурационном файле сайта (допустим, /etc/nginx/sites-enabled/default), в блоке server указываем что-то типа такого:
server {
# Тут то, что было в блоке до ...
# ------------------------------------------------------
# Общий флаг (/var/www/html/maintenance.flag)
# ------------------------------------------------------
if (-f /var/www/html/maintenance.flag) {
return 503;
}
# ------------------------------------------------------
# Общий обработчик для сервиснах сообщений
# ------------------------------------------------------
error_page 500 502 503 504 =200 @maintenance;
error_page 408 =200 @maintenance;
# этот блок оставляем для физической подачи файла
location @maintenance {
root /var/www/html;
rewrite ^(.\*)$ /maintenance.html break;
}
# Тут то, что было в блоке после ...
}
После чего применим:
nginx -t && nginx -s reload
Как это работает?
Как только ты создашь файл /var/www/html/maintenance.flag, Nginx увидит это и переведет кластер в состояние обслуживание MAINTENANCE). Вместо сайта на фронтэнд-часть будет отправлен файл /var/www/html/maintenance.html.
Аналогичная ситуация будет в случае получения ошибок с HTTP-кодами 500, 502, 503, 504 и 408
Бонус: MAINTENANCE-мод только для конкретных IP-адресов
Если ты знаешь адреса хостов администраторов, и при этом не хочешь чтобы они видели сообщение об обслуживании (т.е. им отдавался сайт в том состоянии, в котором он на самом деле сейчас находится, в конфиге сайта используй такую конструкцию:
# ------------------------------------------------------
# Добавился новый блок для директив MAP и GEO,
# т.к. они не могут располагаться в server
# ------------------------------------------------------
http {
# Кто считается админом (НЕ видит maintenance)
geo $is_admin {
default 0;
10.10.10.0/24 1; # вся подсеть
11.111.111.11 1; # твой внешний IP
127.0.0.1 1; # локально
}
# Принимаем решение, показывать ли maintenance этой сессии
map "$maintenance:$is_admin" $show_maintenance {
default 0; # по умолчанию – не показывать
"1:0" 1; # если техработы включены И клиент не админ → показываем
}
}
# ------------------------------------------------------
# Основной блок сайта
# ------------------------------------------------------
server {
# Тут то, что было в блоке до ...
# ------------------------------------------------------
# Флаг техработ (оставляем как есть, но через переменную)
set $maintenance 0;
if (-f /var/www/html/maintenance.flag) { set $maintenance 1; }
# Блокировка до прокси/роутинга (генерируем исключение, если инстанс в состоянии MAINTENCE)
if ($show_maintenance) { return 503; }
# Блок, который указывает на конкретный файл со страницей обслуживания
error_page 503 =200 @maintenance;
location @maintenance {
root /var/www/html;
rewrite ^(.*)$ /maintenance.html break;
}
# ------------------------------------------------------
# Тут то, что было в блоке после ...
}
И аналогично предыдущему примеру, после этого выполни:
nginx -t && nginx -s reload
Как проверить что все работает?
Чтобы перевести хост в состояние "на тех. обслуживании", выполни команду:
touch /var/www/html/maintenance.flag
Чтобы снять его с него и прекратить показывать страницу формата "у нас ТО":
rm /var/www/html/maintenance.flag
При этом, никакие службы перезапускать не нужно.
Дата обновления мануала: 23.08.25
Быстрая настройка времени на Linux-хосте
Реально быстро
Чтобы настроить время на хосте, выполни:
timedatectl set-timezone Europe/Moscow
Чтобы получить список зон:
timedatectl list-timezones
Установка WayDroid
Какую проблему будем решать?
В какой-то момент мне понадобилось заиметь Android-хост в своей инфраструктуре. Да не простой, а с GooglePlay и, по возможности, не заставляющий ЦПУ биться в конвульсиях при запуске первого инстанса.
Основная загвоздка тут в том, что решение должно так или иначе вписываться в мои реалии - наличие хоста Proxmox и нескольких (десятков) ВМ. Вариант с LXC я отмел сразу, т.к. мне нужен был ванильный Android, а не его сборка. К тому же, я бы не хотел на одной ноде держать и LXC, и ВМ.
И я нашел решение этой проблемы - WayDroid.
Как мы ее будем решать?
В первую очередь, определимся с вводными. Я буду:
- Создавать ВМ на Ubuntu Server 24.04 (это важно, т.к. на момент написания статьи это самый стабильный дистрибутивы под такие цели);
- Устанавливать и настраивать систему как в GUI, так и в CLI (я почему отдельно что и где нужно сделать).
Процесс установки
Создание ВМ
В PVE:
- Загружаем и импортируем образ Ubuntu Server 22.04 в PVE (прямая ссылка на ISO-образ Ubuntu)
- Создаем ВМ с произвольным названием указываем наш ISO как загрузочный (установочный) образ
- Перед ее запуском, сверяемся с параметрами:
- Ресурсы (рекомендовано под 1 ВМ, можно и больше):
- ЦПУ, ядер: 4 (важно: тип ЦПУ - "host")
- ОЗУ, ГБ: 4
- СХД, ГБ: 75 (в моем случае, SSD)
- Machine: q35
- Display: SPICE
- Ресурсы (рекомендовано под 1 ВМ, можно и больше):
- (ВАЖНО!) Устанавливаем сетевой адаптер - для установки нам понадобится интернет
Получается примерно так:
Дальше, устанавливаем ОС. Из важных моментов:
- Ubuntu наверняка вам предложит обновить установочный скрипт на более новую версию. В моменте, лучше согласиться с обновлением. Если вы не согласились - ничего страшного, на работу системы это не влияет;
- На моменте разметки, выделите LVM больше места (чтобы убрать свободное место, в установщике оно помечено как "free space");
- Мы установили Display типа SPICE (qxl). Это сыграет в плане производительности. Но с нюансом...
Важно понимать, что привычная Console в ВЕБ (сверху справа) через него не откроется - в моем случае, PVE 9 версии по этой кнопке начинает скачивать обрывки сертификата (и это не зависит от браузера). Не пугайтесь, установку вы можете продолжить из одноименной вкладки слева:
В крайнем случае, вы можете временно изменить значение параметра на default.
TIP: Переходите к следующему после того, как вы установите систему
Установка WayDroid
Установка GUI
Важно понимать, что WayDroid работает исключительно в GUI. При том, только на Wayland.
Поэтому первым шагом его необходимо установить. Делается это 3 командами:
# Опционально: установка времени (в примере - Москва)
timedatectl set-timezone Europe/Moscow
# Синхронизация с репозиториями
apt update
# Установка последних обновлений пакетов на ОС (может занять какое-то время)
apt upgrade -y
# Установка доп. ПО - оно будет полезно дальше
apt install -y curl wget nano
# Установка GUI на Wayland (может занять какое-то большое кол-во времени)
apt install -y ubuntu-desktop-minimal
После установки, нам нужно сделать еще 1 вещь: заставить ОС грузиться не в терминальную, а в графическую оболочку. Делается это двумя командами, включая перезагрузку.
sudo systemctl set-default graphical.target
reboot
Ждем, пока ВМ перезагрузится. Затем - первый вход.
Важно!
- Входим от обычного пользователя (не root);
- При входе, после выбора пользователя, нажимаем на "шестеренку" (⚙️) и выбираем Ubuntu (без каких-либо приписок).
После первого входа в ОС, в терминале проверяем, что мы используем верный GUI-менеджер:
echo $XDG_SESSION_TYPE
# Ожидаем вывод:
# wayland
Если вывод совпадает (wayland) - переходим к дальнейшей установке. В противном случае - гуглим.
Установка эмулятора WayDroid
Здесь все просто. Все делается в 3 команды:
sudo apt install -y curl
curl https://repo.waydro.id | sudo bash
sudo apt install -y waydroid
После этого, у вас должна появиться команда "waydroid" в CLI.
Запуск первого приложения
Скачивание образов
Скачиваем образы Android:
# Без сервисов GooglePlay
sudo waydroid init
# С сервисами GooglePlay (не самый стабильный, но возможный вариант)
sudo waydroid init -s GAPPS
В случае, если вы получаете таймаут:
Вы можете попробовать временно отключить IPv6. Часто, это решает проблему:
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
После чего повторите команды выше.
Запуск приложения
Если здесь будут использованы команды из CLI - они выполняются в GUI-сессии, а не через SSH!
Теперь, мы можем попробовать запустить свой первый инстанс. В CLI кастуем в разных терминалах:
waydroid session start
waydroid show-full-ui
И ждем, пока Waydroid запустит сессию. Поздравляю! Вам удалось. Выглядеть оно будет примерно вот так:
Главный экран
Браузер
Сайд-бар (доступен по клику сверху)
Настройки
Галерея
RDP на Linux (GNOME Remote Desktop)
Этот метод подойдет для GUI Gnome с менеджером Wayland: встает "болт-он", работает быстрее и стабильнее аналогов.
Однако, с другими GUI и менеджерами сессий могут возникнуть проблемы - там лучше не экспериментировать с GRDP, а рассмотреть аналоги
Что нужно знать перед установкой
Перед тем как идти по этому мануалу, вам нужно знать МИНУСЫ этого подхода:
- Подключения будет работать только до того момента, пока работает пользовательская сессия. Если пользователь уйдет в Lock-screen, удаленный доступ пропадет
- MSTSC.exe (удаленный рабочий стол Windows) не сможет работать с этим решением в связи с отсутствием у последнего механизма NLA (Network Layer Authentification). MObaXTerm - тоже не сможет подключиться, как использует MSTSC.exe под капотом
- GNOME Remote Desktop НЕ является полноценным сервером RDP, и уж тем более не дружит с Windows-way клиентами. С этим решением в целом работает очень мало какое ПО под Windows, но в списке таких звездочек - Remmina и FreeRDP
Из плюсов - скорость и стабильность работы (по слухам, я так и не удосужился проверять).
Установка
Шаг 1. Установка GRDP
apt update
apt install -y gnome-remote-desktop
Все дальнейшие шаги будут выполняться от пользователя user1 (не от root!) и в GUI (не в терминале!)
Шаг 2. Включаем RDP
gsettings set org.gnome.desktop.remote-desktop.rdp enable true
Шаг 3. Создание учетных данных для пользователя
Для создания пользователя, выполняем команду от имени того, кто будет заходить на сервер (не root!). Например, для user1:
su - user1
# Чтобы пароль не светился и не попадал в history:
read -rp "RDP user: " RDP_USER
read -srp "RDP pass: " RDP_PASS; echo
grdctl rdp set-credentials "$RDP_USER" "$RDP_PASS"
unset RDP_USER RDP_PASS
Шаг 4. Разрешаем подключение
grdctl rdp enable
# Убираем view-only-mod (блокировка ввода через RDP)
grdctl rdp disable-view-only
# Автозагрузка
systemctl --user enable --now gnome-remote-desktop
Шаг 5. Заранее открываем порт 3389 (пример с UFW)
Если порт 3389 будет закрыть, подключения не произойдет. Чтобы открыть его через UFW, выполняем:
sudo ufw allow 3389/tcp
sudo ufw reload
# Посмотреть список правил можно так:
sudo ufw status
Шаг 6. Скорее всего, в логах gnome-remote-desktop при первом запуске вы увидите что-то такое:
● gnome-remote-desktop.service - GNOME Remote Desktop
Loaded: loaded (/usr/lib/systemd/user/gnome-remote-desktop.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2026-01-14 20:30:15 MSK; 3min 8s ago
Main PID: 31760 (gnome-remote-de)
Tasks: 4 (limit: 9379)
Memory: 15.4M
CPU: 319ms
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/gnome-remote-desktop.service
└─31760 /usr/libexec/gnome-remote-desktop-daemon
янв 14 20:30:15 home-srv-android systemd[1683]: Starting GNOME Remote Desktop...
янв 14 20:30:15 home-srv-android systemd[1683]: Started GNOME Remote Desktop.
янв 14 20:30:15 home-srv-android gnome-remote-de[31760]: RDP TLS certificate and key not configured properly
янв 14 20:32:50 home-srv-android systemd[1683]: Started GNOME Remote Desktop.
янв 14 20:33:23 home-srv-android systemd[1683]: Started GNOME Remote Desktop.
И порт 3389 слушаться не будет, сколько бы вы не перезагружали службу.
Проблема тут вот в этой ошибке:
RDP TLS certificate and key not configured properly
И тут есть 2 варианта решения проблемы: использовать самоподписанный или нормальный TLS-сертификат.
Решение через self-signed TLS
1. Генерируем самоподписанный сертификат в ~/.local/share/grd:
mkdir -p ~/.local/share/grd
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout ~/.local/share/grd/rdp.key \
-out ~/.local/share/grd/rdp.crt \
-subj "/CN=home-srv-android"
chmod 600 ~/.local/share/grd/rdp.key
Важно:
1. Ключ должен быть без пароля (unencrypted PEM);
2. Файлы должны быть читаемы пользователем, который будет подключаться (иначе демон их не прочтет).
Если у вас есть свой TLS-сертификат, просто подложите его в директорию:
1. Сертификат: ~/.local/share/grd/rdp.crt
2. Приватный ключ: ~/.local/share/grd/rdp.key
Или измените команду ниже
2. Привязываем его к GRDP:
grdctl rdp set-tls-cert ~/.local/share/grd/rdp.crt
grdctl rdp set-tls-key ~/.local/share/grd/rdp.key
grdctl rdp enable
3. Перезапускаем сервис и проверяем, что он запущен
# Перезапуск делается от имени ЭТОГО ПОЛЬЗОВАТЕЛЯ, А НЕ ROOT!
systemctl --user restart gnome-remote-desktop
# Проверяем статус
grdctl status
# Проверяем, что порт слушается
sudo ss -ltnp | grep -E ':3389'
ВАЖНОЕ ПРАВИЛО!
ВСЕ команды типа grdctl и systemctl --user выполняются ТОЛЬКО от того пользователя, кому они предназначены (без sudo). В нашем случае, это user1
Как настроить Outlook 2019 для использования моего почтового сервера
Процесс добавления УЗ
В Outlook при добавлении учётки:
- Введите email
- Выберите "Дополнительные параметры"
- Далее, “Настроить вручную”. Там выберите IMAP
Дальше нужно настроить параметры почты:
Входящая (IMAP)
- Сервер: mail.elijahkamsky.ru
- Порт: 993
- Шифрование: SSL/TLS
- Метод входа: Обычный пароль
- Имя пользователя: полный email (*****@elijahkamsky.ru)
- Исходящая (SMTP)
Сервер: mail.elijahkamsky.ru
- Порт: 587
- Шифрование: STARTTLS
- Требуется проверка подлинности: Да
- Имя пользователя/пароль: те же
В дополнительных настройках Outlook проверьте, чтобы SPA (Secure Password Authentication) было выключено. Outlook иногда пытается его включить.
Внешний вид почтовика
Пример сообщения на внешние электронные адреса
Настройка скрипта автозапуска через SystemD-юнит (серверный подход)
Предисловие
Какие существуют подходы к автозагрузке в Linux?
Есть 3 самых популярных подхода решения задачи "я хочу, чтобы скрипт запускался после перезагрузки сервера":
- Через файл rc.local - устаревший подход, да и правка системных конфигов, даже таких, всегда чревата;
- @reboot в cron - это явный костыль, хотя многие со мной не согласятся;
- Скрипты в духе "sleep 60 && ..." - это зло. Они не дают контроля и работают буквально "на шару". Слишком много ответственности для простого bash-скрипта.
ИМХО автора
Для серверов единственно правильный путь решения этой проблемы сейчас - systemd unit
Почему это лучше, чем альтернативные подходы?
Подход автора лучше, потому что он:
- Дает значительно больше контроля над сервисом: его остановка, внеплановый запуск или сбор информации о его работе становятся значительно проще для администратора - для этого будут использоваться стандартные утилиты, к которым все привыкли (systemctl и journalctl);
- Разделяет ответственность: логика скрипта находится отдельно от логики, которая ответственна за его запуск;
- Позволяет менять логику скрипта отдельно, не меняя любой внешней логики (атомарность в подходе).
Пример настройки скрипта для перезапуска сервисов
Пишем сервисный файл (SystemD-юнит)
Я редактирую файл юнита. Допустим, он будет называться post-reboot-tasks, тогда:
nano /etc/systemd/system/post-reboot-tasks.service
В самом юните я делаю следующее:
[Unit]
Description=Post reboot tasks
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/scripts/post-reboot.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Почему это работает?
Тут у читателя может возникнуть вопрос: А с чего ты взял, что оно сработает только при перезагрузке? Где ты здесь указываешь, что он будет выполнен именно и только после ребута системы?
Отвечаю:
В SystemD это делается не отдельной строкой в духе "AfterReboot=true", а самим фактом, что юнит включён в target, который достигается при загрузке. В этом примере вот эта строчка и говорит, "запускать при старте системы":
WantedBy=multi-user.target
Когда вы делаете:
systemctl enable post-reboot-tasks
SystemD создаёт symlink юнита в:
/etc/systemd/system/multi-user.target.wants/post-reboot-tasks.service
А multi-user.target - это стандартная "нормальная загрузка сервера без GUI" (условно, runlevel=3). Каждый раз, когда система доходит до этого Target'а (то есть после ребута/бут-цикла), SystemD подхватит юнит и запустит его.
Здесь "после ребута" = "после каждой загрузки", т.е. юнит будет выполняться каждый boot.
Подготовка скрипта
Здесь все просто. Я:
- Создаю директорию, которую ранее указывал в сервисном файле;
- Создаю и заполняю скрипт внутри нее (см. пример ниже);
- Выдаю необходимые права для запуска скрипта;
- Применяю изменения для SystemD, чтобы юнит появился в списке и попал в пул активных.
Команда, которой я это сделал:
mkdir -p /opt/scripts/ && \
nano /opt/scripts/post-reboot.sh && \
chmod +x /opt/scripts/post-reboot.sh && \
systemctl daemon-reexec && systemctl daemon-reload && \
systemctl enable --now post-reboot-tasks
Теперь, когда создан скрипт и юнит-файл попал в реестр SystemD, он будет запускаться при каждой перезагрузке
Пример наполнения скрипта
Перейдем к примерам скриптов. В моем случае, я бы хотел автоматический перезапуска ряда сервисов после того, как система загрузилась и получила IP-адрес. Именно это я и делаю здесь:
В случае, если вы захотите изменить его логику, вы можете просто отредактировать скрипт. Перезапускать юнит или указывать SystemD о его изменении не нужно.
Важное уточнение, которое я вынесу отдельно
Скрипт будет запускаться при загрузке системы только в том случае, если его статус - enabled. В противном случае скрипт не попадет в автозагрузку. Проверить это можно командой:
systemctl status post-reboot-tasks
Генерация нескольких сотен тестовых файлов в цикле (Linux)
Генерация
- С небольшой датой (501 файл):
for i in {1..501}; do echo $(date +%T_%s) > "${i}.txt" ; echo "$i"; done
# Sample content:
# 12:22:07_1769505327
- С произвольной неповторяющейся датой (501 файл):
for i in {1..501}; do echo "$(date +%T_%s && openssl rand -base64 24)" > "${i}.txt" ; echo -e "Generated \e[1;34m${i}.txt\e[0m"; done
# Sample content:
# 12:22:07_1769505727
# vCN9MgYoOe1PLQiitXEGhSWqaCcFwqba
- С уникальными именами и данными (501 файл):
for i in {1..501}; do fname="$(openssl rand -hex 32)"; echo -e "$(date +%T_%s)\n$fname" > "${fname}${i}.txt" ; echo -e "Generated \e[1;34m${fname}${i}.txt\e[0m"; done
# Sample content:
# 12:28:20_1769506100
# 48a2a31880bb565ca40baa3147a35c70786dfcca689de6f453690dd8e9f24d30
Создание архива
Создание архива одной командой (все txt-файлы в этой директории):
zip -r archive_$(date +%F).zip ./*.txt
Бонус: сильно ускоренная версия скрипта
#!/bin/bash
# Генерация 10k файлов
count=10000
for ((i=1;i<=count;i++)); do
fname="$(openssl rand -hex 32)"
printf -v now '%(%T_%s)T' -1 # builtin, без fork()
printf '%s\n%s\n' "$now" "$fname" "$RANDOM" > "${fname}${i}.txt"
# логирует реже, иначе терминал станет бутылочным горлышком
(( i % 500 == 0 )) && echo "Generated $i files..."
done
echo -e "\e[1;34mDONE!\e[0m"
Но здесь есть нюанс: оно работает быстро только на SSD, т.к. каждый файл нужно записать на диск. И тут упор будет уже в скорость чтения/записи. И разница критичная. Вот пример для SSD:
На HDD этот процесс будет выполняться более чем в 100 раз дольше.
Бонус 2: подсчет кол-ва файлов в директории
Чтобы посчитать кол-во файлов в директории, можно использовать:
ls -1 | wc -l
Но с этой командой есть нюанс: она считает вообще все записи, включая директории. Чтобы посчитать только файлы с расширением .txt можно использовать GREP:
ls -1 | grep '.txt' | wc -l
Разница видна здесь:
RU locale setup on Linux (debian-based only)
sudo localectl set-locale LANG=ru_RU.UTF-8 && export LANG=ru_RU.UTF-8 LC_ALL=ru_RU.UTF-8
Скрипт для генерации произвольной структуры файлов и папок
Скрипт генерации
Новая версия
#!/usr/bin/env bash
set -euo pipefail
# === Настройки (можно переопределять через ENV) ===
ROOT_DIR="${ROOT_DIR:-./upload_test_tree}" # куда генерить
TOTAL_ITEMS="${TOTAL_ITEMS:-40000}" # всего объектов (файлы+папки)
FILES_COUNT="${FILES_COUNT:-10000}" # сколько файлов
MAX_DEPTH="${MAX_DEPTH:-8}" # максимальная глубина (root=0)
PRINT_EVERY="${PRINT_EVERY:-1000}" # прогресс раз в N созданий
# === Валидация ===
if (( FILES_COUNT > TOTAL_ITEMS )); then
echo "FILES_COUNT ($FILES_COUNT) не может быть больше TOTAL_ITEMS ($TOTAL_ITEMS)" >&2
exit 1
fi
DIRS_COUNT=$((TOTAL_ITEMS - FILES_COUNT)) # директорий (включая root? нет)
# Мы отдельно создаём root, поэтому целевая сумма директорий = DIRS_COUNT + 1
rm -rf -- "$ROOT_DIR"
mkdir -p -- "$ROOT_DIR"
# Списки директорий и их глубин
dirs=("$ROOT_DIR")
depths=(0)
created_dirs=1 # root уже создан
created_files=0
created_total=0 # будем считать созданные "объекты" сверх root
mkname() {
local prefix="$1"
# Быстро и "достаточно уникально" без urandom/openssl:
# счётчик + BASHPID + RANDOM
echo "${prefix}_${BASHPID}_${created_total}_${RANDOM}${RANDOM}"
}
pick_parent_for_file() {
echo $(( RANDOM % ${#dirs[@]} ))
}
pick_parent_for_dir() {
local tries=8 idx
while (( tries-- > 0 )); do
idx=$(( RANDOM % ${#dirs[@]} ))
if (( depths[idx] < MAX_DEPTH )); then
echo "$idx"
return
fi
done
for idx in "${!dirs[@]}"; do
if (( depths[idx] < MAX_DEPTH )); then
echo "$idx"
return
fi
done
# если всё на MAX_DEPTH — просто вернём любой
echo $(( RANDOM % ${#dirs[@]} ))
}
echo "ROOT_DIR=$ROOT_DIR"
echo "TOTAL_ITEMS=$TOTAL_ITEMS (dirs=$DIRS_COUNT, files=$FILES_COUNT), MAX_DEPTH=$MAX_DEPTH"
echo
target_dirs=$((DIRS_COUNT + 1)) # + root
target_files=$FILES_COUNT
target_total=$TOTAL_ITEMS
while (( (created_dirs + created_files) < target_total )); do
remaining_dirs=$(( target_dirs - created_dirs ))
remaining_files=$(( target_files - created_files ))
make_dir=0
if (( remaining_dirs > 0 && remaining_files > 0 )); then
roll=$(( RANDOM % (remaining_dirs + remaining_files) ))
if (( roll < remaining_dirs )); then make_dir=1; fi
elif (( remaining_dirs > 0 )); then
make_dir=1
else
make_dir=0
fi
if (( make_dir == 1 )); then
pidx="$(pick_parent_for_dir)"
parent="${dirs[pidx]}"
dname="$(mkname d)"
newdir="$parent/$dname"
mkdir -p -- "$newdir"
dirs+=("$newdir")
depths+=($(( depths[pidx] + 1 )))
((++created_dirs))
else
pidx="$(pick_parent_for_file)"
parent="${dirs[pidx]}"
fname="$(mkname f)"
: > "$parent/$fname.bin"
((++created_files))
fi
((++created_total))
if (( created_total % PRINT_EVERY == 0 )); then
echo "Created: total=$((created_dirs + created_files)) dirs=$created_dirs files=$created_files (dir_pool=${#dirs[@]})"
fi
done
echo
echo "DONE."
echo "Path: $ROOT_DIR"
echo "Dirs created (incl root): $created_dirs"
echo "Files created: $created_files"
echo "Total created: $((created_dirs + created_files))"
Пример запуска скрипта
Пример генерируемой структуры
Старая версия
#!/usr/bin/env bash
set -euo pipefail
# === Настройки (можно переопределять через ENV) ===
ROOT_DIR="${ROOT_DIR:-./upload_test_tree}" # куда генерить
TOTAL_ITEMS="${TOTAL_ITEMS:-40000}" # всего объектов (файлы+папки)
FILES_COUNT="${FILES_COUNT:-10000}" # сколько файлов
MAX_DEPTH="${MAX_DEPTH:-8}" # максимальная глубина (root=0)
PRINT_EVERY="${PRINT_EVERY:-1000}" # прогресс раз в N созданий
# === Валидация ===
if (( FILES_COUNT > TOTAL_ITEMS )); then
echo "FILES_COUNT ($FILES_COUNT) не может быть больше TOTAL_ITEMS ($TOTAL_ITEMS)" >&2
exit 1
fi
DIRS_COUNT=$((TOTAL_ITEMS - FILES_COUNT)) # директорий (включая root? нет)
# Мы отдельно создаём root, поэтому целевая сумма директорий = DIRS_COUNT + 1
rm -rf -- "$ROOT_DIR"
mkdir -p -- "$ROOT_DIR"
# Списки директорий и их глубин
dirs=("$ROOT_DIR")
depths=(0)
created_dirs=1 # root уже создан
created_files=0
created_total=0 # будем считать созданные "объекты" сверх root
mkname() {
local prefix="$1"
# Быстро и "достаточно уникально" без urandom/openssl:
# счётчик + BASHPID + RANDOM
echo "${prefix}_${BASHPID}_${created_total}_${RANDOM}${RANDOM}"
}
pick_parent_for_file() {
echo $(( RANDOM % ${#dirs[@]} ))
}
pick_parent_for_dir() {
local tries=8 idx
while (( tries-- > 0 )); do
idx=$(( RANDOM % ${#dirs[@]} ))
if (( depths[idx] < MAX_DEPTH )); then
echo "$idx"
return
fi
done
for idx in "${!dirs[@]}"; do
if (( depths[idx] < MAX_DEPTH )); then
echo "$idx"
return
fi
done
# если всё на MAX_DEPTH — просто вернём любой
echo $(( RANDOM % ${#dirs[@]} ))
}
echo "ROOT_DIR=$ROOT_DIR"
echo "TOTAL_ITEMS=$TOTAL_ITEMS (dirs=$DIRS_COUNT, files=$FILES_COUNT), MAX_DEPTH=$MAX_DEPTH"
echo
target_dirs=$((DIRS_COUNT + 1)) # + root
target_files=$FILES_COUNT
target_total=$TOTAL_ITEMS
while (( (created_dirs + created_files) < target_total )); do
remaining_dirs=$(( target_dirs - created_dirs ))
remaining_files=$(( target_files - created_files ))
make_dir=0
if (( remaining_dirs > 0 && remaining_files > 0 )); then
roll=$(( RANDOM % (remaining_dirs + remaining_files) ))
if (( roll < remaining_dirs )); then make_dir=1; fi
elif (( remaining_dirs > 0 )); then
make_dir=1
else
make_dir=0
fi
if (( make_dir == 1 )); then
pidx="$(pick_parent_for_dir)"
parent="${dirs[pidx]}"
dname="$(mkname d)"
newdir="$parent/$dname"
mkdir -p -- "$newdir"
dirs+=("$newdir")
depths+=($(( depths[pidx] + 1 )))
((++created_dirs))
else
pidx="$(pick_parent_for_file)"
parent="${dirs[pidx]}"
fname="$(mkname f)"
: > "$parent/$fname.bin"
((++created_files))
fi
((++created_total))
if (( created_total % PRINT_EVERY == 0 )); then
echo "Created: total=$((created_dirs + created_files)) dirs=$created_dirs files=$created_files (dir_pool=${#dirs[@]})"
fi
done
echo
echo "DONE."
echo "Path: $ROOT_DIR"
echo "Dirs created (incl root): $created_dirs"
echo "Files created: $created_files"
echo "Total created: $((created_dirs + created_files))"
Пример использования скрипта
Настройки генерации:
ROOT_DIR="${ROOT_DIR:-./upload_test_tree}" # куда генерить
TOTAL_ITEMS="${TOTAL_ITEMS:-40000}" # всего объектов (файлы+папки)
FILES_COUNT="${FILES_COUNT:-10000}" # сколько файлов
MAX_DEPTH="${MAX_DEPTH:-8}" # максимальная глубина (root=0)
PRINT_EVERY="${PRINT_EVERY:-40000}" # прогресс раз в N созданий
Пример использования вывода с такими настройками:
Быстрая активация Windows и MS Office
Для быстрой активации Windows и MS Office:
- Откройте PowerShell от имени администратора;
- Выполните команду:
irm https://get.activated.win | iex
- В открывшемся окне выберите какой продукт вы ходите активировать: Windows или MS Office.
- Выберете тип активации продукта. TIP: наиболее стабильный тип активации - через KMS.
- Следуйте инструкциям далее. Не прерывайте скрипт - дайте ему закончить свою работу. Среднее время активации не превышает 1 минуты
Важно: для этого типа активации нужно стабильное интернет-подключение.
Выпуск и обновление сертификата Let's Encrypt (WildCard) через DNS
Что я хочу сделать?
У меня есть доступ к записям на публичном DNS-сервере, который содержит мой домен. Я хочу перевыпустить TLS-сертификат через CertBot
Как мне это сделать?
На примере домена elijahkamsky.ru:
- В CLI на любом хосте, где установлен CertBot:
certbot certonly \
--manual \
--preferred-challenges dns \
-d elijahkamsky.ru \
-d *.elijahkamsky.ru
- Certbot скажет создать TXT-запись:
_acme-challenge.elijahkamsky.ru
Например:
_acme-challenge.elijahkamsky.ru TXT qwe123asdasd
После добавления записи — сертификат выпустится автоматически.
Где лежит сертификат?
После выпуска сертификат будет в директории:
/etc/letsencrypt/live/elijahkamsky.ru/
Там:
cert.pem
chain.pem
fullchain.pem
privkey.pem
Обычно веб-сервера используют только:
fullchain.pem
privkey.pem
Обратите внимание!
Сертификат будет выпущен только после того, как основная часть публичных DNS-серверов обновит вашу запись.
Полное обновление DNS-записей может занимать вплоть до 48 часов.
Проверить это можно тут: https://dnschecker.org/
Или тут: https://toolbox.googleapps.com/apps/dig/#TXT
Тип записи: TXT
Пример проверки:
Активация Windows и MS Office
"Мне прям по-быстрому для Windows 10 PRO"
В Poweshell от имени Администратора:
slmgr /ipk W269N-WFGWX-YVC9B-4J6C9-T83GX
slmgr /skms kms8.msguides.com
slmgr /ato
slmgr /xpr
Я хочу почитать
Чтобы активировать Windows 10 существует 2 метода:
- Использовать скрипт автоматической активации
В Powershell от имени администратора вставляем:
irm https://get.activated.win | iex
Дальше скрипт все сделает за нас. Ориентируйтесь на меню скрипта и встроенные подсказки для работы с ним
- Активация через службу KMS (вручную)
- Открываем Powershell от имени администратора
- Устанавливаем сервер для верификации ключа KMS:
slmgr /skms kms.lotro.cc
- Устанавливаем ключ продукта:
slmgr /ipk "SERIAL NUMBER HERE"
Замените SERIAL NUMBER HERE в команде на 1 из доступных ключей в зависимости от редакции Windows:
# kms.lotro.cc
Home/Core TX9XD-98N7V-6WMQ6-BX7FG-H8Q99
Home/Core (Country Specific) PVMJN-6DFY6-9CCP6-7BKTT-D3WVR
Home/Core (Single Language) 7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH
Home/Core N 3KHY7-WNT83-DGQKR-F7HPR-844BM
Professional W269N-WFGWX-YVC9B-4J6C9-T83GX
Professional N MH37W-N47XK-V7XM9-C7227-GCQG9
Enterprise NPPR9-FWDCX-D2C8J-H872K-2YT43
Enterprise N DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4
Education NW6C2-QMPVW-D7KKK-3GKT6-VCFB2
Education N 2WH4N-8QGBV-H22JP-CT43Q-MDWWJ
Enterprise 2015 LTSB WNMTR-4C88C-JK8YV-HQ7T2-76DF9
Enterprise 2015 LTSB N 2F77B-TNFGY-69QQF-B8YKP-D69TJ
Enterprise 2016 LTSB DCPHK-NFMTC-H88MJ-PFHPY-QJ4BJ
Enterprise 2016 LTSB N QFFDN-GRT3P-VKWWX-X7T3R-8B639
# kms8.msguides.com
Home: TX9XD-98N7V-6WMQ6-BX7FG-H8Q99
Home N: 3KHY7-WNT83-DGQKR-F7HPR-844BM
Home Single Language: 7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH
Home Country Specific: PVMJN-6DFY6-9CCP6-7BKTT-D3WVR
Professional: W269N-WFGWX-YVC9B-4J6C9-T83GX
Professional N: MH37W-N47XK-V7XM9-C7227-GCQG9
Education: NW6C2-QMPVW-D7KKK-3GKT6-VCFB2
Education N: 2WH4N-8QGBV-H22JP-CT43Q-MDWWJ
Enterprise: NPPR9-FWDCX-D2C8J-H872K-2YT43
Enterprise N: DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4
Например, slmgr /ipk W269N-WFGWX-YVC9B-4J6C9-T83GX
- Принудительно проверяем установленный ключ на KMS-сервере
slmgr /ato
Если сервер ответил положительно и ваша ревизия Windows совпадает с той, к которой вы указали ключ, то вы увидите окно с подтверждением активации Windows:
Настройка мультиязычности и поддержки кириллицы в Linux
Этот мануал описывает, как корректно настроить отображение русских символов (UTF-8) в Linux-системе: консоль, SSH, приложения, systemd, Docker и Ansible.
🔍 Как понять, есть ли проблема
Типичные симптомы:
- вместо кириллицы —
????или кракозябры localeвыводитCилиPOSIX- команды с русскими именами файлов работают некорректно
- в логах systemd или приложений битая кодировка
Проверка текущего состояния:
locale
❌ Плохо:
LANG=C
LC_ALL=
✅ Хорошо:
LANG=ru_RU.UTF-8
LC_ALL=
📦 Установка необходимых локалей
Debian / Ubuntu
apt update
apt install -y locales
RHEL / Rocky / Alma / CentOS Stream
dnf install -y glibc-langpack-ru
Arch Linux
pacman -S glibc
🧩 Генерация локали ru_RU.UTF-8
Debian / Ubuntu
nano /etc/locale.gen
Раскомментировать:
ru_RU.UTF-8 UTF-8
Сгенерировать:
locale-gen
🌐 Установка локали по умолчанию
localectl set-locale LANG=ru_RU.UTF-8
🖥 Принудительная установка (cron, systemd, Ansible)
/etc/environment
LANG=ru_RU.UTF-8
LC_ALL=ru_RU.UTF-8
🔐 SSH
В /etc/ssh/sshd_config:
AcceptEnv LANG LC_*
systemctl restart sshd
🧰 systemd
В unit-файле:
[Service]
Environment="LANG=ru_RU.UTF-8"
Environment="LC_ALL=ru_RU.UTF-8"
systemctl daemon-reexec
🐳 Docker
ENV LANG=ru_RU.UTF-8
ENV LC_ALL=ru_RU.UTF-8
🛠 Ansible
ansible_env:
LANG: ru_RU.UTF-8
LC_ALL: ru_RU.UTF-8
🧪 Проверка
echo "Привет, мир"
touch тест.txt
ls
✅ Итог
Если локаль сгенерирована и LANG=ru_RU.UTF-8 задан глобально — кириллица будет работать везде.