Скрипт автоматизированного харденинга Ubuntu 22.04 / 24.04 / 26.04 для виртуальных серверов.
Запускается от root сразу после получения чистого VPS/VDS и проводит полную настройку безопасности в один проход.
Основан на практиках CIS Ubuntu Linux Benchmark с поправками на специфику VPS: физического доступа нет, консоль — только через панель провайдера, поэтому все меры, способные «уронить» сервер в офлайн (паника ядра при сбое аудита, пароль GRUB, агрессивный blacklist драйверов), либо смягчены, либо сознательно исключены. Каждое такое решение задокументировано в разделе Сознательные отклонения от CIS.
- Что нового в v2
- Требования
- Быстрый старт
- Переменные конфигурации
- Интерактивные вопросы и контрольные точки
- Что делает скрипт
- 0. Предстартовые проверки и обновление системы
- 1. Имя хоста, FQDN, DNS, часовой пояс, NTP
- 2. Генерация SSH-ключа
- 3. Создание пользователя admin
- 4. Настройка SSH
- 5. Блокировка root
- 6. Брандмауэр UFW
- 7. Fail2ban
- 8. Защита cron
- 9. Защита разделов
- 10. Параметры ядра (sysctl)
- 11. Blacklist модулей ядра
- 12. Отключение core dump
- 13. Auditd
- 14. AppArmor
- 15. Настройка sudo
- 16. Политика паролей
- 17. Права на системные файлы
- 18. Удаление телеметрии
- 19. Автоматические обновления
- 20. Удаление приватного ключа
- Сознательные отклонения от CIS
- Подключение после настройки
- Диагностика и мониторинг
- Что требует перезагрузки
- Восстановление при потере доступа
- Повторный запуск скрипта
- Ручное включение форвардинга
| Исправление | Суть |
|---|---|
| ssh.socket | Ubuntu 22.10+ запускает SSH через socket-активацию, при которой Port из sshd_config игнорируется. Скрипт отключает ssh.socket — это истинная причина проблемы «порт не поменялся» |
| Контрольная точка входа | Проверка SSH-входа под admin теперь до блокировки root, а не в конце скрипта |
| Auditd: причина backlog exceeded | Удалены правила аудита socket/connect — они генерировали тысячи событий/сек. Увеличение backlog лечило симптом, а не причину |
| Auditd 3.x | Убраны dispatcher = /sbin/audispd и disp_qos — удалены из auditd 3.x (Ubuntu 22.04+), с ними демон мог не стартовать |
| Auditd: arch=b32 | Ключевые правила продублированы для 32-битных syscalls — без этого аудит обходился 32-битными вызовами |
| /etc/ssh 755 | Было chmod 700 /etc/ssh — это ломало исходящие ssh/scp/git для непривилегированных пользователей (нет доступа к ssh_config) |
| logrotate для audit удалён | auditd ротирует логи сам (max_log_file_action=ROTATE); параллельный logrotate давал двойную ротацию и разрывы аудита |
| Тип ключа на выбор | ed25519 (рекомендуется) или ecdsa-521 |
| passwd в цикле | 3 ошибки ввода пароля больше не роняют скрипт из-за set -e |
| chrony minsources | При одном пользовательском NTP-сервере ставится minsources 1 — иначе время никогда не считалось бы синхронизированным |
| UFW: опрос портов | Перед ufw reset показываются слушающие порты и можно открыть дополнительные (80, 443 и т.д.) |
| noexec /tmp и apt | Временный каталог apt перенесён в /var/lib/apt/tmp — иначе ночные unattended-upgrades могли падать |
| userns под ядро | kernel.unprivileged_userns_clone пишется только если существует в ядре; на 24.04+/26.04 используется apparmor_restrict_unprivileged_userns |
| Повторный запуск | При immutable-правилах auditd (-e 2) секция 13 пропускается с предупреждением вместо аварийного завершения |
| usermod при повторном запуске | Существующий пользователь теперь гарантированно добавляется в sudo |
| rsyslog в зависимостях | Минимальные образы 24.04+ могут идти без rsyslog, на котором построено логирование sudo |
| Лог выполнения + trap | Весь вывод дублируется в /root/hardening_<дата>.log; при ошибке trap показывает номер строки |
| Drop-in c Port | Конфликтующие файлы /etc/ssh/sshd_config.d/* с директивой Port перемещаются в /root/sshd_config.d.bak |
| История root | Симлинк .bash_history → /dev/null убран — это антифорензика; аудит команд ведёт auditd (priv_exec) |
| Мелочи | dpkg -s вместо dpkg -l; фикс приоритета операторов в bind-mount /var/tmp; валидация NTP-серверов; AddressFamily в переменной |
| Параметр | Значение |
|---|---|
| ОС | Ubuntu 22.04 / 24.04 / 26.04 LTS |
| Платформа | VPS / VDS (KVM, XEN, VMware, Hyper-V) |
| Пользователь | root |
| Сеть | Доступ в интернет для установки пакетов |
| Доступ | VNC или консоль провайдера на случай потери SSH |
Версия ОС проверяется при старте по /etc/os-release: для 22.04 / 24.04 / 26.04 скрипт продолжает без вопросов, для любой другой — предупреждает и запрашивает подтверждение.
⚠️ Перед запуском убедитесь, что у вас открыта консоль провайдера (VNC/KVM). Если SSH-сессия оборвётся в процессе, вы сможете продолжить через неё. Особенно это важно на этапах смены порта SSH и блокировки root.
# Скачать скрипт на сервер
wget -O ubuntu_hardening.sh https://raw.githubusercontent.com/borross/ubuntu_hardening/refs/heads/main/ubuntu_hardening.sh
# Проверить содержимое перед запуском
less ubuntu_hardening.sh
# Запустить
bash ubuntu_hardening.shСкрипт интерактивный — задаёт вопросы по порту SSH, типу ключа, имени хоста, FQDN, DNS, часовому поясу, NTP, дополнительным портам UFW и паролю ключа. Всё, что можно пропустить, пропускается нажатием Enter.
Весь вывод дублируется в лог /root/hardening_<дата>_<время>.log — при разборе проблем это первое, куда стоит смотреть. При аварийной остановке trap печатает номер строки, на которой произошёл сбой.
Настраиваются в начале файла скрипта:
ADMIN_USER="admin" # имя создаваемого пользователя
SSH_PORT="20202" # порт по умолчанию (будет переспрошен интерактивно)
KEY_PATH="/root/admin_ssh_key" # путь для сохранения ключевой пары
# Семейство адресов для SSH:
# any — IPv4 + IPv6 (безопасное значение по умолчанию)
# inet — только IPv4 (НЕ ставьте на IPv6-only VPS — потеряете доступ!)
SSH_ADDRESS_FAMILY="any"
# Форвардинг — отключён по умолчанию, включайте осознанно
TCP_FORWARDING="no"
GATEWAY_PORTS="no"
X11_FORWARDING="no"Порядок диалогов при выполнении:
| Этап | Вопрос | По умолчанию |
|---|---|---|
| Предстарт | Порт SSH (1024–65535) | 20202 |
| Предстарт | Тип ключа: ed25519 / ecdsa-521 | ed25519 |
| Секция 1 | Имя хоста, FQDN, DNS (2 адреса), часовой пояс, NTP-серверы | текущие значения / пулы Ubuntu |
| Секция 2 | Пароль приватного ключа (ssh-keygen) | — обязателен |
| Секция 2 | Пауза «скачайте приватный ключ» | — |
| Секция 3 | Пароль пользователя admin (для sudo) |
— обязателен, в цикле |
| Секция 4 | Контрольная точка: «вход под admin работает?» y/n |
n прерывает скрипт до блокировки root |
| Секция 6 | Дополнительные TCP-порты для UFW | только SSH |
| Секция 20 | «Удалить приватный ключ с сервера?» | N (оставить) |
Контрольная точка в секции 4 — главный предохранитель: root блокируется только после того, как вы из нового терминала подтвердили, что ключевой вход под admin на новом порту работает.
Нумерация секций соответствует выводу скрипта.
Проверки: запуск от root, версия Ubuntu, наличие /etc/os-release.
Расчёт лимитов логирования — две независимые метрики, посчитанные до начала изменений:
Квота логов = свободное место на разделе /var/log ÷ 10 (10%, зажато 50–2048 МБ)
делится на 5 файлов ротации auditd
Audit backlog = RAM_МБ × 8 (зажато 8192–65536)
| RAM VPS | Backlog |
|---|---|
| 512 МБ – 1 ГБ | 8192 (минимум) |
| 2 ГБ | 16384 |
| 4 ГБ | 32768 |
| 8 ГБ+ | 65536 (максимум) |
Backlog — буфер в оперативной памяти ядра, поэтому считается от RAM, а не от диска. Расчёт выводится на экран при старте.
Обновление и пакеты: apt-get upgrade в неинтерактивном режиме (--force-confdef/--force-confold — не задавать вопросов о конфигах) и установка:
ufw fail2ban auditd audispd-plugins libpam-pwquality
unattended-upgrades apt-listchanges acl curl wget gnupg2 chrony rsyslog
rsyslog указан явно: минимальные cloud-образы Ubuntu 24.04+ могут поставляться без него, а на нём построено логирование sudo (секция 15). Гипервизор определяется через systemd-detect-virt.
Единственный полностью интерактивный раздел; каждый пункт пропускается через Enter.
Имя хоста — hostnamectl set-hostname, валидация по RFC 1123 (только a-z, 0-9, дефис, ≤63 символов). /etc/hosts обновляется автоматически — без этого sudo выдаёт раздражающее unable to resolve host.
FQDN — валидация по RFC 1123 (метки через точки, TLD ≥2 символов), запись в /etc/hosts:
127.0.1.1 server01.example.com server01
Благодаря такой записи hostname --fqdn и hostname -s работают корректно без реального DNS.
DNS — основной и резервный IPv4-адрес, каждый валидируется (формат + октеты 0–255):
| Провайдер | Основной | Резервный |
|---|---|---|
| Cloudflare | 1.1.1.1 |
1.0.0.1 |
8.8.8.8 |
8.8.4.4 |
|
| Quad9 | 9.9.9.9 |
149.112.112.112 |
| Yandex | 77.88.8.8 |
77.88.8.1 |
Если активен systemd-resolved — настройка через drop-in /etc/systemd/resolved.conf.d/custom-dns.conf с DNS-over-TLS (opportunistic) и DNSSEC (allow-downgrade). Иначе — прямая запись в /etc/resolv.conf с защитой от перезаписи DHCP-клиентом (chattr +i).
Часовой пояс — timedatectl set-timezone, значение проверяется по timedatectl list-timezones перед применением.
NTP через chrony. Chrony предпочтительнее systemd-timesyncd на VPS:
| systemd-timesyncd | chrony | |
|---|---|---|
| Точность | средняя | высокая |
| После долгого простоя VPS | медленно догоняет | быстрый makestep |
| Нестабильная сеть | хуже | maxdistance отсекает плохие источники |
| Логирование | минимальное | measurements/statistics/tracking |
По умолчанию — пулы 0-3.ubuntu.pool.ntp.org + time.cloudflare.com с NTS (аутентифицированный NTP). Пользовательские серверы валидируются (IPv4 или hostname по RFC 1123); невалидные пропускаются, при нуле валидных — откат на серверы по умолчанию. minsources подстраивается автоматически: 1 при единственном сервере, иначе 2. chronyc слушает только localhost. После запуска — принудительный chronyc makestep и вывод статуса синхронизации.
systemd-timesyncd отключается, чтобы не конфликтовать с chrony.
# ed25519 (по умолчанию, рекомендуется)
ssh-keygen -t ed25519 -f /root/admin_ssh_key -C "admin@<host>-<дата>"
# ecdsa-521 (по выбору — совместимость со старыми требованиями)
ssh-keygen -t ecdsa -b 521 -f /root/admin_ssh_key -C "admin@<host>-<дата>"Ключ генерируется с паролем — ssh-keygen запрашивает его интерактивно. Ed25519 предложен по умолчанию: быстрее, компактнее, без зависимости от NIST-кривых; ECDSA-521 оставлен для сред, где он требуется явно.
| Файл | Права | Назначение |
|---|---|---|
/root/admin_ssh_key |
600 |
приватный ключ — скачать и удалить в секции 20 |
/root/admin_ssh_key.pub |
644 |
публичный ключ — остаётся на сервере |
Содержимое обоих ключей выводится в терминал, скрипт делает паузу до подтверждения, что приватный ключ сохранён:
# из второго терминала:
scp root@<IP>:/root/admin_ssh_key ./admin_ssh_keyuseradd -m -s /bin/bash admin # с -g admin, если группа уже существует
usermod -aG sudo admin # выполняется ВСЕГДА, и при повторном запускеОсобенности:
- на Ubuntu группа
adminсуществует по умолчанию (историческая, до появления группыsudo) — в этом случае она используется как primary group через-g, а не создаётся заново; usermod -aG sudoвынесен из ветки создания: если пользователь существовал, но не был в sudo, раньше он туда не попадал;passwdвыполняется в циклеuntil— три ошибки ввода не прерывают скрипт (пароль нужен только для sudo, не для SSH);- публичный ключ добавляется в
~/.ssh/authorized_keys, дубликаты убираются черезsort -u; права700на каталог и600на файл; владелец — реальная primary group пользователя (id -gn), а не имя пользователя вслепую.
Файл: /etc/ssh/sshd_config. Перед изменением — резервная копия sshd_config.bak.<timestamp>.
Права на каталог и файлы (CIS 5.1.1–5.1.2):
/etc/ssh 755 (700 ломало ssh-КЛИЕНТ для обычных пользователей —
им нужен доступ к /etc/ssh/ssh_config)
/etc/ssh/sshd_config 600
ssh_host_*_key 600
ssh_host_*_key.pub 644
Ключевые параметры:
| Параметр | Значение | CIS | Обоснование |
|---|---|---|---|
Port |
выбранный | — | Нестандартный порт снижает шум автосканеров |
AddressFamily |
any |
— | Переменная SSH_ADDRESS_FAMILY; inet опасен на IPv6-only VPS |
PermitRootLogin |
no |
5.1.20 | Прямой вход root запрещён |
PasswordAuthentication |
no |
— | Только ключевая аутентификация |
AuthenticationMethods |
publickey |
— | Явное требование ключа |
PermitEmptyPasswords |
no |
5.1.19 | |
LoginGraceTime |
30 |
5.1.13 | 0 означало бы «без лимита» — баг оригинальной конфигурации |
MaxAuthTries |
3 |
5.1.16 | Максимум попыток на соединение |
MaxSessions |
5 |
5.1.17 | |
MaxStartups |
5:30:20 |
5.1.18 | Защита от медленных атак на handshake |
ClientAliveInterval / CountMax |
300 / 2 |
5.1.7 | Разрыв при 10 мин простоя |
AllowUsers |
admin |
5.1.4 | Только указанный пользователь |
AllowTcpForwarding |
no |
5.1.5 | Туннели обходят firewall; переменная в начале скрипта |
GatewayPorts / X11Forwarding |
no |
5.1.22 | |
LogLevel |
VERBOSE |
5.1.14 | Fingerprint ключа в каждой записи лога |
Banner |
/etc/ssh/my_banner |
5.1.3 | Юридическое предупреждение |
DebianBanner / VersionAddendum |
no / none |
— | Скрыть версию ОС от сканеров |
Криптография (CIS 5.1.6 / 5.1.12 / 5.1.15) — только современные алгоритмы:
HostKeyAlgorithms : ecdsa-sha2-nistp521, ssh-ed25519, rsa-sha2-512/256
KexAlgorithms : curve25519-sha256, ecdh-sha2-nistp521
Ciphers : chacha20-poly1305, aes256-gcm, aes128-gcm
MACs : hmac-sha2-512-etm, hmac-sha2-256-etm
Порядок применения — важен, здесь были главные баги прошлых версий:
- Drop-in файлы: если в
/etc/ssh/sshd_config.d/*есть директиваPort(cloud-образы кладут туда свои конфиги), такие файлы перемещаются в/root/sshd_config.d.bak— иначе они молча переопределяли наш порт; sshd -t— валидация синтаксиса до рестарта;- Отключение
ssh.socket: Ubuntu 22.10+ по умолчанию запускает SSH через socket-активацию systemd, при которой порт задаёт сокет, аPortизsshd_configигнорируется. Это и было истинной причиной проблемы «порт не поменялся». Скрипт переключает на классическийssh.service; - Имя службы (
ssh/sshd) определяется по реально запущенным юнитам; - Рестарт и подтверждение порта через
ss -tlnp; - Контрольная точка: скрипт останавливается и требует подтвердить вход под
adminиз нового терминала. Ответnпрерывает выполнение — root остаётся разблокированным, можно чинить и перезапускать.
passwd -l rootДополнительно su ограничивается членами группы sudo через pam_wheel.so в /etc/pam.d/su (CIS 5.2.7) — непривилегированный пользователь не сможет даже пытаться подобрать пароль root через su.
Перед сбросом правил скрипт показывает все слушающие порты (ss -tlnp) и предлагает перечислить дополнительные TCP-порты — раньше ufw --force reset молча закрывал порты уже работающих сервисов (веб-сервер и т.п.).
ufw default deny incoming # CIS 4.2.7
ufw default allow outgoing
ufw default deny forward
ufw limit <PORT>/tcp # rate-limit iptables: 6 попыток за 30 сек → блок
ufw allow <extra>/tcp # порты, указанные пользователем (с валидацией)ufw limit вместо ufw allow для SSH — встроенная защита от брутфорса, работающая ещё до Fail2ban.
💡 VPS-специфика: Hetzner / DigitalOcean / Vultr предоставляют собственный firewall в панели — продублируйте правила и там. Это защита в глубину: UFW внутри гостя, firewall провайдера снаружи.
Файл: /etc/fail2ban/jail.local
| Параметр | Значение | Комментарий |
|---|---|---|
backend |
systemd |
чтение journald напрямую, не зависит от файлов логов |
banaction |
ufw |
баны через тот же firewall |
port |
выбранный SSH-порт | подставляется автоматически |
maxretry / bantime (sshd) |
3 / 24h |
|
findtime |
10m |
окно подсчёта попыток |
Итоговая эшелонированная защита SSH: ufw limit (сеть) → Fail2ban (журнал) → MaxAuthTries/MaxStartups (сам sshd).
chown root:root /etc/crontab /etc/cron.{hourly,daily,weekly,monthly,d}/
chmod og-rwx /etc/crontab /etc/cron.{hourly,daily,weekly,monthly,d}/Создаются /etc/cron.allow и /etc/at.allow (600), содержащие только root; файлы cron.deny / at.deny удаляются (CIS 2.4.1). При наличии обоих файлов allow имеет приоритет, но явное удаление deny исключает неоднозначность. Обработка at — только если он установлен.
Файл: /etc/fstab (резервная копия перед изменением). CIS 1.1.2.x.
| Раздел | Флаги | От чего защищает |
|---|---|---|
/tmp |
nosuid,nodev,noexec,relatime |
Выполнение залитых скриптов/бинарников из /tmp |
/dev/shm |
nosuid,nodev,noexec |
Исполнение из разделяемой памяти |
/var/tmp |
bind → /tmp |
Наследует все флаги /tmp |
/home |
nosuid,nodev,noexec |
Исполнение из домашних каталогов |
Детали реализации:
seclabelв/dev/shmубрана — опция работает только с SELinux, Ubuntu использует AppArmor;- применение через
mount -o remountбез перезагрузки; исправлен баг приоритета операторов (a || b && c), из-за которого для/var/tmpвыводилось «примонтирован», даже когда mount не выполнялся; - если
/homeне отдельный раздел (типично для VPS) — создаётся systemd unitremount-home.service, который перемонтирует/homeс нужными флагами при каждой загрузке; - совместимость noexec /tmp с apt: часть maintainer-скриптов dpkg выполняется из временного каталога. Скрипт переносит его в
/var/lib/apt/tmp(/etc/apt/apt.conf.d/99tempdir) — без этого ночные unattended-upgrades могли падать сPermission denied.
Файл: /etc/sysctl.d/99-hardening.conf, применение через sysctl --system.
Сетевая безопасность (CIS 3.3.x):
| Параметр | Значение | Защита от |
|---|---|---|
rp_filter (all/default) |
1 |
IP-спуфинга |
ip_forward, ipv6 forwarding |
0 |
использования как роутера |
tcp_syncookies + syn_retries=2, synack_retries=2, max_syn_backlog=4096 |
1 |
SYN-flood |
accept_redirects, secure_redirects (v4+v6) |
0 |
ICMP MITM |
send_redirects |
0 |
перенаправления трафика |
icmp_echo_ignore_broadcasts, icmp_ignore_bogus_error_responses |
1 |
Smurf-атак |
log_martians |
1 |
(логирование подозрительных источников) |
accept_ra (v6) |
0 |
Router Advertisement атак |
tcp_rfc1337 |
1 |
TIME-WAIT assassination |
Безопасность ядра:
| Параметр | Значение | CIS | Описание |
|---|---|---|---|
kernel.yama.ptrace_scope |
1 |
1.5.2 | ptrace только от родителя; не 2 — иначе на VPS нельзя диагностировать через strace/gdb |
kernel.dmesg_restrict |
1 |
— | dmesg недоступен обычным пользователям |
kernel.kptr_restrict |
2 |
— | адреса ядра скрыты из /proc и /sys |
kernel.sysrq |
0 |
— | Magic SysRq отключён (есть консоль провайдера) |
kernel.randomize_va_space |
2 |
1.5.1 | ASLR максимальный |
fs.suid_dumpable |
0 |
1.5.3 | core dump setuid-процессов запрещён |
kernel.core_pattern |
|/bin/false |
— | третий уровень блокировки dump (см. секцию 12) |
fs.protected_{hardlinks,symlinks} |
1 |
— | классические /tmp-атаки через ссылки |
fs.protected_{fifos,regular} |
2 |
— | атаки через FIFO/файлы в sticky-каталогах |
User namespaces — подбирается под ядро автоматически:
kernel.unprivileged_userns_clone = 0— пишется только если параметр существует (/proc/sys/kernel/...). Это Debian/Ubuntu-патч, отсутствующий на новых ядрах — раньше sysctl молча выдавал ошибку;kernel.apparmor_restrict_unprivileged_userns = 1— механизм ядер Ubuntu 24.04+/26.04, пишется при наличии.
⚠️ Ограничение userns ломает rootless Docker/Podman. Если используете контейнеры без root — закомментируйте соответствующие строки в/etc/sysctl.d/99-hardening.confи выполнитеsysctl --system.
Файл: /etc/modprobe.d/hardening-blacklist.conf. Каждый модуль блокируется парой blacklist + install <mod> /bin/false — вторая директива не даёт подгрузить модуль даже явным modprobe.
Файловые системы с историей уязвимостей (CIS 1.1.1.x): cramfs, freevxfs, jffs2, hfs, hfsplus, udf. Вектор атаки — автоматическое монтирование специально созданных образов через udisks2/automount.
Сетевые протоколы с историей CVE (CIS 3.2.1–3.2.4): dccp, sctp, rds, tipc. На VPS практически никогда не нужны.
VPS-специфика: USB-storage и Firewire не блокируются — на виртуальной машине этих устройств нет, а агрессивный blacklist может задеть virtio-драйверы некоторых провайдеров.
ext*/vfat/overlay/squashfsне трогаются — это сломало бы корневую ФС, EFI, snap и контейнеры.
После записи — update-initramfs -u -k all; полная сила blacklist — после перезагрузки.
Тройная блокировка (CIS 1.5.3) — три механизма покрывают разные классы процессов:
| Уровень | Файл | Покрывает |
|---|---|---|
hard core 0 |
/etc/security/limits.conf |
PAM-сессии (интерактивные входы) |
Storage=none, ProcessSizeMax=0 |
/etc/systemd/coredump.conf.d/disable.conf |
процессы под systemd |
kernel.core_pattern = |/bin/false |
sysctl (секция 10) | всё остальное |
Core dump процесса может содержать пароли, ключи и другие секреты из памяти — на харденированном сервере они не нужны.
Защита от повторного запуска: если правила уже immutable (auditctl -s → enabled 2), секция пропускается с предупреждением — изменить правила можно только после reboot, а попытка рестарта auditd уронила бы скрипт.
/etc/audit/auditd.conf — лимиты рассчитаны в секции 0:
max_log_file = <квота ÷ 5> МБ num_logs = 5
max_log_file_action = ROTATE log_format = ENRICHED
space_left_action = SYSLOG admin_space_left_action = ROTATE
disk_full_action = ROTATE ← не halt/single: VPS без консоли ушёл бы в офлайн
flush = INCREMENTAL_ASYNC freq = 50
Устаревшие dispatcher = /sbin/audispd и disp_qos отсутствуют — они удалены в auditd 3.x (Ubuntu 22.04+), с ними демон мог не стартовать.
Правила /etc/audit/rules.d/99-hardening.rules:
| Категория | Что отслеживается | Ключ | CIS |
|---|---|---|---|
| Идентификация | /etc/passwd, shadow, group, gshadow |
identity |
6.2.3.8 |
| Привилегии | /etc/sudoers, /etc/sudoers.d/ |
sudoers |
6.2.3.1 |
| SSH | sshd_config, ~admin/.ssh/ |
sshd_config, ssh_keys |
— |
| Cron | все cron-каталоги + /var/spool/cron/ |
cron |
— |
| Права файлов | chmod/chown и варианты, b64 + b32, фильтр auid>=1000 |
file_perm |
6.2.3.9 |
| Повышение привилегий | setuid/setgid (b64+b32); execve при euid=0 от реальных пользователей |
priv_esc, priv_exec |
6.2.3.6 |
| Модули ядра | insmod/rmmod/modprobe + syscalls *_module |
kernel_modules |
6.2.3.19 |
| Сеть | только bind реальных пользователей |
net_bind |
— |
| Загрузчик | /boot/ |
boot |
— |
Принципиальные решения:
- правила
socket/connectнамеренно отсутствуют — аудит каждого сетевого вызова генерировал тысячи событий в секунду и был первопричиной ошибкиaudit: backlog limit exceeded; увеличение буфера лечило симптом. Новый слушающий сокет (bind) — редкое и действительно значимое событие, он оставлен; - дублирование b64 + b32: без правил для
arch=b32аудит тривиально обходился 32-битными syscalls; - фильтр
-F auid>=1000 -F auid!=unsetотсекает шум системных демонов, оставляя действия реальных пользователей; -b <backlog>— рассчитан из RAM;--backlog_wait_timeубран (значение1из старой версии уменьшало ожидание и увеличивало потери — комментарий был неверным);-f 1— при сбое аудита писать в syslog, не паника ядра (-f 2): на VPS без физической консоли паника означает недоступный сервер;-e 2в конце — правила неизменяемы до перезагрузки: злоумышленник не отключит аудит без reboot, который сам оставит след.
Ротация: только средствами auditd. Отдельный /etc/logrotate.d/audit не создаётся и удаляется, если был — auditd держит файл открытым и ротирует сам; параллельный logrotate с рестартом демона давал двойную ротацию и разрывы аудита.
Просмотр событий:
ausearch -k identity | tail -20 # изменения passwd/shadow
ausearch -k sudoers | tail -20 # изменения sudoers
ausearch -k ssh_keys | tail -20 # изменения SSH-ключей
ausearch -k priv_exec | tail -20 # команды от root реальными пользователями
ausearch -k net_bind | tail -20 # новые слушающие сокеты
aureport --summary # сводка
auditctl -s # backlog, lost, enabledapparmor/apparmor-utils доустанавливаются при отсутствии (проверка через dpkg -s). Все профили из /etc/apparmor.d/ переводятся в режим enforce (CIS 1.3.1.4): в отличие от complain, нарушения не только логируются, но и блокируются. Проверка: aa-status.
Файл: /etc/sudoers.d/99-hardening (права 440, visudo -c до применения):
| Настройка | Значение | Назначение |
|---|---|---|
timestamp_timeout=5 |
5 минут | сессия sudo истекает быстро |
!visiblepw |
— | пароль не отображается при вводе |
secure_path |
фиксированный | защита от подмены системных утилит через PATH |
Логирование — через rsyslog (/etc/rsyslog.d/50-sudo.conf): все события sudo дублируются в /var/log/sudo.log (права 600). Формат записи:
Jul 14 03:12:44 server sudo: admin : TTY=pts/0 ; PWD=/home/admin ; USER=root ; COMMAND=/bin/bash
Опции log_output, logfile, requiretty сознательно не используются: первые две требуют I/O-плагин sudo, не входящий в пакет Ubuntu, третья удалена из sudo 1.9.x — с ними sudo -i падал с unknown setting.
Файл: /etc/security/pwquality.conf (CIS 5.3.3.2.x):
| Параметр | Значение | Смысл |
|---|---|---|
minlen |
16 |
минимальная длина |
dcredit/ucredit/lcredit/ocredit |
-1 |
минимум 1 цифра / заглавная / строчная / спецсимвол |
maxrepeat |
3 |
не более 3 одинаковых символов подряд |
dictcheck |
1 |
проверка по словарю cracklib |
gecoscheck |
1 |
запрет слов из GECOS (имени пользователя) |
SSH-вход по паролю отключён — политика защищает пароль для sudo и любые будущие локальные УЗ.
chmod 600 /etc/shadow /etc/gshadow # CIS 7.1.3–7.1.4
chmod 644 /etc/passwd /etc/group # CIS 7.1.1–7.1.2
chmod 700 /bootSUID снимается с редко используемых утилит chfn, chsh, newgrp — каждая проверяется на существование и наличие бита.
Изменение v2: симлинк
~root/.bash_history → /dev/nullубран. Отключение истории — антифорензика: при инциденте она помогает атакующему скрыть следы больше, чем защищает владельца. Команды с правами root аудирует auditd (ключpriv_exec) — этот журнал, в отличие от истории shell, защищён immutable-правилами.
| Пакет | Назначение |
|---|---|
ubuntu-report |
отчёты об использовании системы |
popularity-contest |
статистика установленных пакетов |
apport |
сбор и отправка crash-репортов |
whoopsie |
отправщик crash-репортов в Canonical |
Проверка через dpkg -s (не dpkg -l, который возвращал успех даже для удалённых пакетов в статусе rc), затем purge + autoremove + autoclean. Сервисы apport/whoopsie отключаются на случай остатков; при наличии snap отключаются его метрики.
/etc/apt/apt.conf.d/50unattended-upgrades:
Разрешённые источники — -security, ESM Apps/Infra (Ubuntu Pro), -updates (многие исправления безопасности приходят именно через -updates, а не -security).
Blacklist — пакеты критичных сервисов, обновление которых должно быть ручным и контролируемым:
docker* kube* postgresql* mysql* mariadb* nginx* apache2* redis* mongodb*
| Параметр | Значение | Причина |
|---|---|---|
AutoFixInterruptedDpkg |
true |
исправлять прерванные установки |
MinimalSteps |
true |
атомарные шаги — безопасное прерывание |
Remove-Unused-Dependencies |
true |
без orphan-пакетов |
Automatic-Reboot |
false |
неожиданный reboot продакшн-VPS недопустим |
Mail / MailOnlyOnError |
root / true |
письмо только при ошибках, а не после каждого прогона |
Расписание — systemd drop-in (не перезапись unit-файла, чтобы обновления пакетов не затирали настройку):
/etc/systemd/system/apt-daily.timer.d/override.conf → 03:00 ± 10 мин
/etc/systemd/system/apt-daily-upgrade.timer.d/override.conf → 03:30 ± 10 мин
Перед новым OnCalendar дефолтные значения сбрасываются пустыми строками — иначе systemd объединяет расписания и таймер срабатывает по обоим. RandomizedDelaySec=10m — чтобы VPS одного провайдера не стучались в репозитории одновременно. Проверка: systemctl list-timers apt-daily*.
Финальный шаг — предложение надёжно затереть приватный ключ:
shred -vuz /root/admin_ssh_key # многократная перезапись + удалениеТолько после явного подтверждения y. Если ключ ещё не скачан — ответ N, ключ остаётся, скрипт напоминает команду для ручного удаления. Публичный ключ (.pub) остаётся на сервере в любом случае.
Эти решения оправданы спецификой VPS/VDS — это не пробелы, а осознанный выбор:
| Отклонение | CIS | Причина |
|---|---|---|
disk_full_action = ROTATE (не halt/single) |
6.2.2.3 | halt = недоступный VPS без физической консоли |
auditd -f 1 (syslog), не -f 2 (паника) |
— | паника ядра при сбое аудита уронит VPS в офлайн |
Нет аудита socket/connect |
— | тысячи событий/сек, переполнение backlog; bind покрывает значимые случаи |
| USB/Firewire не в blacklist | 1.1.1 | на виртуалке устройств нет; риск задеть virtio-драйверы |
chrony вместо systemd-timesyncd |
2.3 | точнее и быстрее восстанавливается на VPS |
ptrace_scope = 1, не 2 |
1.5.2 | сохранить strace/gdb для диагностики без физического доступа |
kernel.sysrq = 0 |
— | SysRq не нужен (есть консоль провайдера) и является вектором атаки |
Automatic-Reboot = false |
— | неконтролируемый reboot продакшн-VPS недопустим |
| Пароль GRUB не ставится | 1.4.1 | без IP-KVM усложняет восстановление через консоль провайдера |
| userns-ограничения легко отключаемы | — | не ломать rootless-контейнеры без ведома администратора |
| История root не отключается | — | отключение — антифорензика; аудит ведёт auditd |
# Прямое подключение (пароль ключа при каждом входе)
ssh -p <PORT> -i ./admin_ssh_key admin@<IP>
# Через ssh-agent (пароль ключа один раз за сессию)
eval $(ssh-agent)
ssh-add ./admin_ssh_key
ssh -p <PORT> admin@<IP>~/.ssh/config на клиентской машине:
Host myserver
HostName <IP>
Port <PORT>
User admin
IdentityFile ~/.ssh/admin_ssh_key
AddKeysToAgent yesПосле этого достаточно: ssh myserver
# Лог выполнения самого харденинга
ls -la /root/hardening_*.log
# SSH
journalctl -u ssh -u sshd -f # живой лог
ss -tlnp | grep <PORT> # слушает ли порт
systemctl status ssh.socket # должен быть disabled/inactive!
grep -r "^Port" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null
# Fail2ban
fail2ban-client status sshd # забаненные IP
fail2ban-client set sshd unbanip <IP> # разбанить вручную
# UFW
ufw status verbose
# Auditd
auditctl -s # backlog, lost (должно быть 0), enabled
ausearch -k identity | tail -20
ausearch -k ssh_keys | tail -20
ausearch -k priv_exec | tail -20
ausearch -k net_bind | tail -20
aureport --summary
# AppArmor
aa-status
# NTP / Chrony
chronyc tracking # источник и погрешность
chronyc sources -v
timedatectl show
# Разделы
findmnt /tmp /var/tmp /dev/shm /home # проверка флагов noexec/nosuid/nodev
# sudo
tail -20 /var/log/sudo.log
# Автообновления
systemctl list-timers apt-daily*
cat /var/log/unattended-upgrades/*.log- Blacklist модулей ядра — DCCP, SCTP, RDS, TIPC и редкие ФС
- Ограничения user namespaces — на части ядер применяются только после reboot
/var/tmpbind-mount — если не применился в текущей сессии- Любые изменения правил auditd — при действующем immutable-режиме (
-e 2)
reboot
# затем проверка доступности:
ssh -p <PORT> -i ./admin_ssh_key admin@<IP>Все изменяемые конфиги бэкапятся с меткой времени. Через консоль провайдера (VNC/KVM):
# SSH: откат конфига
cp /etc/ssh/sshd_config.bak.<timestamp> /etc/ssh/sshd_config
systemctl restart ssh
# SSH: вернуть перемещённые drop-in (если они были нужны)
cp /root/sshd_config.d.bak/* /etc/ssh/sshd_config.d/ 2>/dev/null
# Разблокировать root (для входа через консоль провайдера)
passwd -u root && passwd root
# fstab: откат (если система не грузится из-за раздела — режим recovery)
cp /etc/fstab.bak.<timestamp> /etc/fstab
# UFW: экстренно выключить
ufw disable
# Fail2ban: снять бан со своего IP
fail2ban-client set sshd unbanip <ваш_IP>
# Разбор причин: лог харденинга
less /root/hardening_*.logЕсли SSH не поднимается после правок — смотреть journalctl -xe -u ssh -u sshd и проверить, не включился ли обратно ssh.socket (systemctl status ssh.socket).
Скрипт можно запускать повторно, с оговорками:
- auditd: пока правила immutable (
enabled 2), секция 13 пропускается с предупреждением; файлы в/etc/audit/при этом уже содержат нужную конфигурацию и применятся после reboot; - root заблокирован — запускайте через
sudo -iиз-подadmin; - дубликаты не плодятся:
authorized_keysдедуплицируется черезsort -u, записи127.0.1.1в/etc/hostsзаменяются, fstab-строки перезаписываются,usermod -aG sudoидемпотентен; ufw --force resetснова сбросит правила — дополнительные порты придётся указать заново в диалоге секции 6.
Если сервер используется как VPN-сервер или прокси:
# Вариант 1: в переменных в начале скрипта перед запуском
TCP_FORWARDING="yes"
GATEWAY_PORTS="yes" # только если нужен проброс портов наружу
# Вариант 2: после запуска — отредактировать sshd_config
sed -i 's/^AllowTcpForwarding no/AllowTcpForwarding yes/' /etc/ssh/sshd_config
systemctl restart ssh
# Плюс разрешить форвардинг в UFW и sysctl:
ufw route allow in on eth0
sysctl -w net.ipv4.ip_forward=1
# и закрепить в /etc/sysctl.d/99-hardening.conf (net.ipv4.ip_forward = 1)