Представьте: новый сервер появился в интернете. Через 10 минут к нему уже пытаются подключиться боты — перебирают пароли, стучатся на порт 22. Без правильной настройки SSH это не вопрос «взломают ли», а вопрос «когда». С правильной — атака останавливается еще на подходе, не долетев до экрана ввода пароля.
SSH (Secure Shell) — главный инструмент удаленного администрирования Linux-систем. Поверх него работает Ansible, строятся туннели к закрытым базам данных, организуется bastion-доступ к внутренней инфраструктуре. Разобраться с ним один раз — значит решить сразу десяток операционных задач.
Что такое SSH и зачем он нужен
Протокол SSHv2 и история
В 1995 году финский исследователь Тату Илонен написал первую реализацию SSH как замену небезопасному Telnet. SSHv1 быстро получил широкое распространение — и почти сразу обнаружились серьезные дыры в протоколе. SSHv2 разрабатывался в IETF как отдельный, несовместимый с v1 протокол; в 2006 стандартизирован в RFC 4251–4254 и вытеснил SSHv1. SSHv1 сегодня отключен по умолчанию в любом нормальном клиенте.
Современная реализация — OpenSSH. Версия 9.x выпущена в 2022 году и идет штатно в любом свежем дистрибутиве Linux. На macOS OpenSSH встроен. На Windows 10 и Windows Server 2019 появился как системный компонент — больше не нужно ставить сторонний PuTTY только ради базовой команды ssh.
SSH vs telnet vs RDP
Telnet — это как разговор по громкой связи в переполненном офисе: все слышно. Пароль, команды, вывод — все летит по сети открытым текстом. Любой, кто может прослушать трафик между клиентом и сервером, получает полный доступ к сессии. RDP решает другую задачу — графический удаленный рабочий стол для Windows. SSH — консоль Linux с полным шифрованием канала.
На legacy-оборудовании Telnet встречается до сих пор — обычно на старых управляемых коммутаторах или промышленных контроллерах. Везде, где есть выбор, давно используют SSH.
Архитектура клиент-сервер
На сервере живет демон sshd — он постоянно слушает входящие соединения на порту 22 (или другом, если настроено иначе). Клиент — это ваш терминал с командой ssh. Сервер ничего не инициирует сам; всегда первым ходит клиент.
Что происходит в момент подключения:
- Клиент устанавливает TCP-соединение с портом сервера.
- Стороны обмениваются версиями протокола и списком поддерживаемых алгоритмов.
- Происходит криптографический обмен ключами по Diffie-Hellman или ECDH — после этого шага весь трафик идет в зашифрованном виде, даже пароль.
- Клиент проходит аутентификацию — по паролю или ключу.
- Открывается канал: интерактивная сессия, туннель или передача файлов.
Настройки сервера — в /etc/ssh/sshd_config. Персональная конфигурация клиента — в ~/.ssh/config.
Первое подключение к серверу
Команда ssh user@server
Минимальная команда для подключения к удаленному серверу:
ssh user@192.168.1.100
Нестандартный порт указывается флагом -p:
ssh -p 2222 user@192.168.1.100
Несколько флагов, которые реально нужны в работе:
- -i ~/.ssh/mykey — явно указать файл приватного ключа, если имя нестандартное
- -v / -vvv — подробный вывод всех шагов подключения; спасает при отладке, когда соединение падает без понятной причины
- -N — запустить SSH без открытия сессии, только tunnel; нужен для проброса портов
- -f — уйти в фон после успешного подключения
На свежих образах Ubuntu и Debian пользователь по умолчанию — ubuntu, на Amazon Linux и CentOS — ec2-user или centos, зависит от образа.
Подтверждение fingerprint и known_hosts
Первое подключение к незнакомому серверу выглядит так:
The authenticity of host '192.168.1.100' can't be established.
ED25519 key fingerprint is SHA256:abc123...
Are you sure you want to continue connecting (yes/no)?
Это не баг и не паранойя — это защита от подмены сервера. Злоумышленник не может выдать себя за ваш сервер: у него другой fingerprint, и SSH немедленно это обнаружит. После ответа yes запись попадает в ~/.ssh/known_hosts. Каждое следующее подключение — автоматическая сверка без вопросов.
Если сервер переустановили и ключ сменился — SSH откажет в соединении с предупреждением. Чтобы удалить устаревшую запись:
ssh-keygen -R 192.168.1.100
Подключение из Windows: OpenSSH, PuTTY, MobaXterm
Встроенный OpenSSH в Windows устанавливается через PowerShell одной командой:
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
После этого ssh работает в любом терминале — PowerShell, CMD, Windows Terminal — с тем же синтаксисом, что и в Linux.
PuTTY — классика для тех, кто привык к графическому интерфейсу. Сохранение сессий, визуальная настройка туннелей, управление ключами через PuTTYgen. Один нюанс: PuTTY хранит ключи в формате .ppk, который OpenSSH не читает напрямую. Конвертация — через PuTTYgen: Conversions → Import Key → Export OpenSSH Key.
MobaXterm — когда нужно больше, чем просто терминал. Встроенный SFTP-браузер, X11 forwarding, несколько вкладок, макросы. Популярен у администраторов Windows, которые работают с большим парком Linux-серверов.
Аутентификация по ключам
Парольная аутентификация работает — до тех пор, пока пароль не подберут. Для сервера в публичной сети «до тех пор» — это вопрос нескольких часов или дней. Ключевая аутентификация устроена принципиально иначе: подобрать нечего, потому что секрет вообще никуда не отправляется.
Генерация ключа: ssh-keygen, типы RSA / Ed25519
Утилита ssh-keygen входит в OpenSSH — доступна везде, где есть ssh. Создать ключ Ed25519:
ssh-keygen -t ed25519 -C "your_email@example.com"
Ed25519 работает на эллиптической кривой Curve25519. Размер ключа — 256 бит, но это не значит «слабее RSA»: математика разная. Ed25519 быстрее RSA-4096 при подписи и при проверке, а по стойкости к атакам превосходит его. Mozilla SSH Guidelines называет Ed25519 предпочтительным алгоритмом для новых ключей.
Когда нужна совместимость со старым оборудованием, которое Ed25519 не поддерживает — берите RSA:
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
RSA-4096 надежен, RSA-2048 — минимальный приемлемый размер на сегодня. При генерации ssh-keygen спросит путь (стандарт — ~/.ssh/id_ed25519) и passphrase. Passphrase шифрует приватный ключ прямо на диске: если файл утечет — без фразы он бесполезен.
Копирование на сервер: ssh-copy-id, authorized_keys
Публичный ключ прописывается в файл ~/.ssh/authorized_keys на сервере — в домашней директории того пользователя, под которым планируете заходить. Быстрее всего через ssh-copy-id:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@192.168.1.100
Утилита сама создаст директорию и файл с нужными правами. Если выставлять права вручную:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Это не формальность. Если права выставлены неправильно — sshd молча проигнорирует authorized_keys, ключ не сработает, и вы будете искать проблему часами. Права 777 на ~/.ssh или доступ на запись у посторонних пользователей — sshd расценивает как небезопасную конфигурацию и отказывается от файла.
ssh-agent и passphrase
Passphrase защищает ключ, но вводить ее при каждом подключении неудобно. ssh-agent — процесс, который держит расшифрованный ключ в памяти на время сессии:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Passphrase спрашивается один раз при добавлении. Все соединения в этой сессии — без дополнительных вводов. На macOS агент стартует автоматически, ключи хранятся в системной Keychain: ssh-add --apple-use-keychain ~/.ssh/id_ed25519. На Linux — через systemd user service или .bashrc/.zshrc.
Почему ключи безопаснее паролей
При входе по паролю: клиент отправляет пароль серверу, сервер проверяет хеш. Пароль пересекает сеть — пусть и в зашифрованном виде. При входе по ключу: сервер присылает случайный запрос, клиент подписывает его приватным ключом, сервер проверяет подпись публичным ключом из authorized_keys. Приватный ключ никуда не отправляется — он участвует только в локальном криптографическом вычислении.
Брутфорс ключа Ed25519 — 2^128 вариантов. При всех вычислительных мощностях человечества это недостижимо. Атака возможна только при физическом доступе к файлу ключа плюс знании passphrase — что на порядок сложнее простого перебора пароля.
Базовая безопасность сервера SSH
После любого изменения sshd_config применяйте настройки без обрыва активных сессий:
systemctl reload ssh
Периодический аудит безопасности SSH-конфигурации — один из самых дешевых способов закрыть очевидные дыры до инцидента.
Запретить root-логин (PermitRootLogin no)
Бот, который перебирает пароли, начинает с root — потому что root есть на каждом Linux-сервере и знать имя пользователя не нужно. Одна строка в sshd_config убирает этот вектор полностью:
PermitRootLogin no
Теперь попытка зайти под root завершится отказом на этапе аутентификации. Рутовые операции делаются через sudo от обычного пользователя — это и безопаснее, и оставляет следы в логах.
Отключить пароли (PasswordAuthentication no)
Когда ключи работают — пароли можно выключить полностью:
PasswordAuthentication no
PermitEmptyPasswords no
Проверьте вход по ключу до применения изменений: если ошибиться с порядком — лишитесь доступа к серверу.
Смена стандартного порта 22
Порт 22 — первое, что проверяют автоматические сканеры. Переезд на нестандартный порт не добавляет реальной защиты от целенаправленной атаки, но логи /var/log/auth.log перестанут пухнуть от тысяч строк бессмысленного шума:
Port 2222
Не забудьте открыть новый порт в firewall до перезагрузки sshd: ufw allow 2222/tcp. Иначе заблокируете себя.
AllowUsers / AllowGroups
AllowUsers и AllowGroups сужают круг тех, кому SSH вообще открывает дверь. Пользователи за пределами списка получают отказ еще до проверки пароля или ключа:
AllowUsers admin@192.168.1.* deploy
AllowGroups sshusers
Запись admin@192.168.1.* — пользователь admin принимается только с адресов подсети 192.168.1.x. С любого другого IP — отказ, даже при правильном ключе. Это удобно для серверов, к которым полагается подключаться только из офисной сети или через VPN.
Продвинутая безопасность
Четыре базовые настройки выше закрывают большинство автоматических атак. Для инфраструктуры с требованиями к сертификации или с ценными данными — нужен следующий уровень.
fail2ban: автоматическая блокировка переборов
fail2ban читает /var/log/auth.log в реальном времени и банит IP-адреса, с которых идет перебор. Ставится в одну команду:
apt install fail2ban
Конфигурация для SSH в /etc/fail2ban/jail.local:
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 3600
findtime = 600
Три неудачных попытки за десять минут — и IP летит в бан на час. bantime = -1 — бессрочная блокировка. Посмотреть заблокированных: fail2ban-client status sshd. Разблокировать конкретный IP: fail2ban-client set sshd unbanip 1.2.3.4.
Port knocking и Single Packet Authorization
Port knocking переворачивает логику файрвола: порт SSH закрыт по умолчанию, и никакой сканер его не увидит. Чтобы он открылся, клиент должен «постучать» в нужную последовательность портов в правильном порядке.
На сервере — демон knockd, на клиенте — утилита knock:
knock 203.0.113.10 7000 8000 9000
ssh user@203.0.113.10
После правильной последовательности файрвол временно открывает порт только для IP клиента — обычно на 15-30 секунд. Этого хватает для установки соединения.
Single Packet Authorization через fwknop идет дальше: один зашифрованный UDP-пакет содержит временную метку и одноразовый токен, что исключает replay-атаки, которые теоретически возможны при классическом port knocking.
2FA через PAM (TOTP/HOTP)
Двухфакторная аутентификация добавляет второй рубеж: даже украденный ключ с passphrase не дает доступа без одноразового кода из приложения. TOTP — код, обновляющийся каждые 30 секунд, генерируется в Google Authenticator, Яндекс Ключе, Authy. HOTP — то же самое, но по счетчику событий.
Настройка на Ubuntu:
apt install libpam-google-authenticator
google-authenticator
google-authenticator генерирует QR-код для приложения и сохраняет конфигурацию в домашней директории пользователя. В /etc/pam.d/sshd:
auth required pam_google_authenticator.so
В sshd_config для OpenSSH 8.7 и новее:
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
На OpenSSH до версии 8.7 параметр назывался ChallengeResponseAuthentication yes — синтаксис изменился в 2021 году, старый вариант оставлен для совместимости.
Bastion host (jumphost) и ProxyJump
Классическая схема защиты внутренней инфраструктуры: все серверы закрыты от интернета, единственная точка входа — bastion host. Снаружи виден только он; все остальное достижимо лишь изнутри.
Подключение через jumphost — одна команда:
ssh -J user@bastion user@internal-server
Или постоянная запись в ~/.ssh/config, чтобы не набирать это каждый раз:
Host internal
HostName 10.0.0.50
ProxyJump bastion.example.com
После этого ssh internal — SSH сам прыгнет через bastion, все прозрачно. Цепочки: -J host1,host2 — два промежуточных прыжка. Распространенная ошибка: копировать приватный ключ на bastion. Не нужно. Используйте agent forwarding — и только в блоке конкретного хоста, не глобально: ForwardAgent yes.
Конфигурация client-side
~/.ssh/config — файл, который администраторы часто игнорируют годами, а потом удивляются, зачем они каждый раз вводят длинные команды вручную. Когда парк машин быстро растёт — грамотный ~/.ssh/config экономит часы в неделю.
Файл ~/.ssh/config: алиасы и параметры
Пример конфигурации с несколькими хостами:
Host prod
HostName 203.0.113.10
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519_prod
Host dev
HostName 10.0.0.20
User ubuntu
ProxyJump prod
ssh prod разворачивается в полную команду со всеми параметрами. ssh dev автоматически прыгает через prod. Маска Host *.example.com применяет параметры ко всем хостам домена сразу — удобно для единой политики ключей или таймаутов.
Multiplexing и ControlMaster
Каждая новая SSH-сессия — это полный цикл: TCP-handshake, криптографический обмен, аутентификация. На медленных каналах это секунды. ControlMaster позволяет переиспользовать уже открытое соединение:
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
ControlPersist 10m
Создайте директорию заранее: mkdir -p ~/.ssh/sockets. Первая сессия открывает мастер-сокет, все следующие к тому же хосту подключаются через него мгновенно. ControlPersist 10m держит мастер живым еще 10 минут после последней сессии. Ansible-прогоны на большом парке с ControlMaster работают заметно быстрее.
Keep-alive и ServerAliveInterval
NAT-устройства и файрволы имеют привычку рвать «тихие» TCP-соединения через несколько минут простоя. Сессия зависает — не пишет, не закрывается. Keep-alive решает это:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Клиент шлет пустой пакет каждые 60 секунд. Три пропущенных ответа подряд — соединение считается мертвым и завершается чисто. Без этой настройки «мертвая» сессия может висеть часами у вас. Симметричную проверку со стороны сервера задаёт ClientAliveInterval — именно она освобождает серверные слоты.
SSH-туннели
SSH tunnel — зашифрованная труба между двумя портами. Все, что входит с одного конца, выходит с другого. Применяется везде, где нужен безопасный доступ к чему-то, что торчать в интернете не должно: базы данных, панели мониторинга, внутренние API.
Local forwarding (-L)
Задача: подключиться к PostgreSQL на сервере, не открывая ее порт в интернет. Local forwarding пробрасывает локальный порт через SSH на удаленный хост:
ssh -L 5432:localhost:5432 user@server
После выполнения команды psql -h localhost -p 5432 на вашем компьютере смотрит на PostgreSQL сервера через зашифрованный канал. База данных слушает только 127.0.0.1 на сервере — снаружи недостижима. Формат флага: -L локальный_порт:хост_назначения:порт_назначения. Хост назначения — от имени сервера, не вашей машины.
Remote forwarding (-R)
Зеркальная ситуация: ваша локальная машина за NAT, белого IP нет, но нужно дать коллеге доступ к локальному стенду. Remote forwarding пробрасывает порт сервера на вашу машину:
ssh -R 8080:localhost:3000 user@server
Коллега заходит на порт 8080 сервера — трафик приходит на localhost:3000 вашей машины. Работает через любой NAT и без статического IP.
Dynamic forwarding (-D, SOCKS5)
Вместо проброса конкретного порта — полноценный SOCKS5-прокси на локальной машине. Весь трафик через него идет через сервер:
ssh -D 1080 -N user@server
Флаг -N — не открывать интерактивную сессию, только держать tunnel. Дальше — настроить браузер: в Firefox это Настройки → Прокси → Ручная настройка → SOCKS-хост 127.0.0.1, порт 1080, версия SOCKS v5. Для консольных утилит используют proxychains.
Результат: браузер работает так, как будто он запущен на сервере. Доступ к внутренним сервисам сети, к которым сервер имеет доступ.
Безопасность туннелей и обратные shell
Remote forwarding в чужих руках — инструмент побега из закрытой сети. Скомпрометированная машина внутри периметра самостоятельно устанавливает SSH-соединение наружу и пробрасывает порт. Исходящий трафик файрвол обычно не режет — и получается backdoor, который не видно снаружи.
Если пользователям туннели не нужны — запретить в sshd_config:
AllowTcpForwarding no
GatewayPorts no
PermitTunnel no
SSH в массовом администрировании
Десять серверов — еще можно управлять руками. Пятьдесят — уже нет. Компании, которые прошли аудит ИТ-инфраструктуры, часто обнаруживают одно и то же: SSH используется как терминал для ручных операций, хотя мог бы быть фундаментом автоматизации.
Ansible через SSH
Ansible — система управления конфигурацией без агентов. На управляемых серверах нужны только Python и работающий sshd. Ansible подключается по SSH, выполняет задачи и закрывает соединение. Пример inventory:
[webservers]
web01 ansible_host=10.0.0.10 ansible_user=admin
web02 ansible_host=10.0.0.11 ansible_user=admin
Ключи берутся из ~/.ssh/ или задаются через ansible_ssh_private_key_file. Если в SSH-конфиге включен ControlMaster — Ansible переиспользует уже открытые соединения вместо создания новых, и прогон на 50 серверах идет заметно быстрее.
Аудит и логирование сессий
Каждый вход, каждый неудачный ввод пароля, каждый разрыв соединения фиксируется в /var/log/auth.log (Ubuntu/Debian) или /var/log/secure (RHEL/CentOS). Быстро посмотреть последние события:
grep 'sshd' /var/log/auth.log | tail -50
Для полноценного аудита нужна запись самих сессий — что именно делал пользователь после входа. Это PAM session recording или специализированные инструменты: tlog, Teleport, StrongDM. Без этого невозможно разобрать инцидент постфактум. Для организаций под 152-ФЗ, PCI DSS или объектов КИИ это не опция — требование.
Centralized SSH CA (HashiCorp Vault, signed keys)
При ста серверах и двадцати администраторах authorized_keys превращается в кошмар. Кто куда имеет доступ — никто точно не знает. Уволился сотрудник — надо найти и удалить его ключ вручную на каждой машине. SSH Certificate Authority решает это централизованно.
Схема работы:
- Пользователь запрашивает подписанный сертификат у CA — через HashiCorp Vault SSH Secrets Engine или Step CA.
- CA выдает сертификат с ограниченным сроком: 8 часов, 1 день — как настроено.
- Сервер проверяет подпись CA, а не конкретный ключ из authorized_keys.
- Срок истек — сертификат перестал работать автоматически, без каких-либо действий на сервере.
В sshd_config достаточно одной строки:
TrustedUserCAKeys /etc/ssh/ca_user_key.pub
Этот файл — один и тот же на всех серверах. При отзыве доступа сотруднику нужно только не выдавать ему новых сертификатов: текущий истечет сам.
Типовые ошибки
Хранение приватного ключа без passphrase
Ключ без passphrase на ноутбуке — это как карточка с пин-кодом, лежащая рядом с банковской картой. Если ноутбук украли, резервную копию залили в Dropbox или случайно закоммитили ключ в репозиторий — доступ ко всем серверам мгновенно у того, кто нашел файл.
Минимальная защита: passphrase плюс ssh-agent — passphrase вводится раз в сессию. Максимальная: YubiKey с FIDO2 — приватный ключ хранится в аппаратном чипе, скопировать его невозможно физически.
Открытый порт SSH в публичной сети с паролями
Публичный IP плюс порт 22 плюс PasswordAuthentication yes — это мишень, которую начинают бить в первые минуты. Логи будут полны тысячами строк неудачных попыток: user root from 1.2.3.4, user admin from 5.6.7.8.
Порядок правильной настройки: скопировать ключ через ssh-copy-id, проверить вход по ключу, выставить PasswordAuthentication no, поднять fail2ban. Только в таком порядке — иначе рискуете заблокировать себя.
Игнорирование fail2ban и логов /var/log/auth.log
Логи авторизации — это не архив, который никто не читает. Это источник сигналов: аномальное число отказов с одного IP, попытки под нетипичными именами пользователей, соединения в необычное время. Если fail2ban не стоит, а логи не мониторятся — компрометация через слабый пароль системного пользователя может остаться незамеченной неделями.
Минимум: fail2ban-client status sshd в конце рабочего дня. Нормально: алерт в мессенджер при аномальном росте количества отказов. Серьезно: SIEM с парсингом SSH-логов и детекцией паттернов атак.
Итоги
Минимальная безопасная конфигурация SSH умещается в несколько строк sshd_config: запрет root-логина, отключение паролей, ключ Ed25519 (или RSA-4096, если нужна совместимость), fail2ban на 3 попытки. Это занимает 20 минут и закрывает 95% автоматических атак.
Следующий уровень — bastion host и 2FA — нужен там, где есть требования регуляторов или ценные данные. Централизованный SSH CA на HashiCorp Vault или Step CA оправдан при парке от 20-30 серверов: управление authorized_keys вручную при таком масштабе становится дырой в безопасности само по себе.
Настраиваем SSH-инфраструктуру под ваши требования
Настраиваете доступ по SSH к парку серверов или хотите внедрить bastion host, 2FA и подписанные ключи через CA? Наши инженеры спроектируют безопасную SSH-инфраструктуру, настроят fail2ban и аудит. Оставьте заявку.
Часто задаваемые вопросы
Что такое SSH простыми словами?
SSH (Secure Shell) — протокол для работы с удаленным сервером через зашифрованный канал. Вы вводите команды в своем терминале, они выполняются на сервере, результат приходит обратно — и все это так, что перехватить или подделать данные в пути невозможно.
Чем SSH-ключ безопаснее пароля?
При входе по ключу секрет никуда не уходит: клиент подписывает запрос сервера локально, сервер проверяет подпись. Взломать перебором нереально. Пароль отправляется на сервер и может быть перехвачен или подобран при недостаточной сложности.
Как сгенерировать SSH-ключ?
Командой ssh-keygen -t ed25519 -C "метка" на Linux, macOS или Windows с OpenSSH. Получите два файла: ~/.ssh/id_ed25519 — приватный, никуда не отправляется, и ~/.ssh/id_ed25519.pub — публичный, его копируют на сервер через ssh-copy-id.
Можно ли подключаться по SSH из Windows?
Да, несколькими способами. Встроенный OpenSSH — через PowerShell или Windows Terminal, синтаксис как в Linux. PuTTY — графический клиент с сохранением сессий. MobaXterm — многофункциональный терминал со встроенным файловым менеджером SFTP.
Зачем менять порт SSH с 22?
Боты постоянно сканируют порт 22 по всему диапазону публичных IP. Переезд на нестандартный порт убирает большую часть этого шума из логов. Это не защита от направленной атаки, но в паре с PasswordAuthentication no хорошо работает для снижения фонового шума.
Что такое fail2ban?
Демон, который в реальном времени читает логи авторизации и добавляет в бан IP-адреса, превысившие лимит неудачных попыток. Три ошибки за десять минут — час в блокировке. Настраивается гибко: лимиты, время бана, список исключений.
Что такое jumphost и ProxyJump?
Jumphost (bastion) — сервер-шлюз на границе сети. Снаружи открыт только он; внутренние серверы недостижимы напрямую. ProxyJump автоматизирует прыжок через него: ssh -J bastion internal или запись в ~/.ssh/config — и SSH сам выстраивает цепочку.
Как настроить 2FA для SSH?
Установить libpam-google-authenticator, запустить google-authenticator от имени пользователя, добавить auth required pam_google_authenticator.so в /etc/pam.d/sshd. В sshd_config для OpenSSH 8.7+: KbdInteractiveAuthentication yes и AuthenticationMethods publickey,keyboard-interactive. Вход потребует и ключ, и TOTP-код из приложения.