---
tags: [network, security, tooling]
verified: 2026-09-02
---

# SSH углублённо: config, бастионы, сертификаты, туннели

## Обзор

SSH — самая часто используемая технология из всех, что описаны в этой базе, и обычно самая недоиспользуемая. Типичная практика сводится к `ssh user@host`, копированию ключа через `ssh-copy-id` и правке пары строк в `sshd_config`; всё остальное — мультиплексирование соединений, прыжки через бастион, сертификаты вместо ключей, ограниченные ключи, туннели — остаётся за кадром, хотя решает ровно те задачи, на которые ежедневно тратится время.

Статья идёт со стороны клиента к серверу: сначала `~/.ssh/config`, из которого выводится большая часть удобства, затем ключи и агент, доступ через промежуточные хосты, сертификаты как замена раздаче ключей, туннели и, наконец, серверная конфигурация и отладка.

Чего здесь нет намеренно: базового списка настроек `sshd_config` для защиты сервера — `PermitRootLogin`, `PasswordAuthentication`, тайм-ауты, защита от перебора. Он уже разобран в заметках про [hardening Linux](./linux-security-hardening.md) и [hardening FreeBSD](./freebsd-security-hardening.md); дублировать его смысла нет, и серверный раздел ниже посвящён механике конфигурации, а не перечню параметров.

Версия-якорь — OpenSSH 10.5 (август 2026). Версии указаны там, где возможность появилась недавно: в дистрибутивах живут разные сборки, и часть приёмов ниже на OpenSSH из старого LTS не заработает.

## ~/.ssh/config

### Первое значение выигрывает

Одно правило объясняет большую часть недоразумений с конфигом, и его стоит запомнить дословно: **используется первое найденное значение каждого параметра**. Документация формулирует это прямо и делает из этого вывод — специфичные для хоста объявления должны стоять ближе к началу файла, а общие умолчания в конце.

Интуиция здесь работает наоборот. От конфигурационного файла ожидаешь поведения «последнее переопределяет предыдущее», как в большинстве форматов, и раскладываешь блоки в обратном порядке. Результат — параметр из общего блока `Host *`, стоящего сверху, молча выигрывает у специфичного блока ниже, и правки «не применяются».

```conf
# ПРАВИЛЬНО: от частного к общему
Host prod-db
    HostName 10.0.4.12
    User postgres
    Port 2222

Host *.internal
    User admin
    IdentityFile ~/.ssh/id_ed25519_work

Host *
    ServerAliveInterval 30
    AddKeysToAgent yes
```

Наследования в объектном смысле нет: блоки не вкладываются, а накапливаются. Хост `prod-db` получает `User postgres` из своего блока и `ServerAliveInterval 30` из `Host *`, потому что `ServerAliveInterval` больше нигде не встретился раньше.

`Host` сравнивает с шаблоном имя, которое вы набрали в командной строке, — не с реальным именем и не с адресом. `Match` мощнее: он умеет условия по итоговому имени (`Match host`), по пользователю, по результату выполнения команды (`Match exec`) и их комбинации. Типовое применение — разная конфигурация в зависимости от того, где вы сейчас находитесь:

```conf
Match host *.corp.example.com exec "nc -z -w1 10.0.0.1 22 2>/dev/null"
    ProxyJump none          # внутри офиса бастион не нужен

Match host *.corp.example.com
    ProxyJump bastion.example.com
```

### IdentityFile и IdentitiesOnly

Ошибка `Too many authentication failures` — самая частая и самая непонятная из сообщений SSH, и причина у неё не та, о которой думают. Сервер не отвергает ваш ключ: он отвергает попытку номер шесть, потому что `MaxAuthTries` по умолчанию равен шести, а клиент до этого успел предложить пять чужих ключей.

Механика такая. Клиент перебирает все ключи, какие знает: из агента, из `~/.ssh/config` и стандартные имена в `~/.ssh/`. Каждое предложение — отдельная попытка аутентификации с точки зрения сервера. С десятком ключей в агенте до нужного дело просто не доходит.

Лечится параметром, название которого не намекает на проблему:

```conf
Host prod-db
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes      # предлагать ТОЛЬКО указанный ключ
```

`IdentitiesOnly yes` заставляет использовать только явно заданные файлы, игнорируя всё, что предлагает агент. На машине, где ключей больше двух, это стоит поставить в `Host *` и задавать `IdentityFile` каждому хосту явно.

### ControlMaster: почему это меняет ощущения от работы

Каждое новое соединение SSH — это TCP-рукопожатие, обмен ключами, аутентификация. На локальной сети это десятки миллисекунд, через океан и бастион — секунды. Работа, состоящая из десятка коротких команд подряд, превращается в ожидание.

Мультиплексирование решает это радикально: первое соединение становится «мастером», последующие идут по нему дополнительными каналами и не требуют ни рукопожатия, ни аутентификации.

```conf
Host *
    ControlMaster auto
    ControlPath ~/.ssh/cm/%C
    ControlPersist 10m
```

Каталог нужно создать заранее (`mkdir -p -m 700 ~/.ssh/cm`) — сам SSH его не создаст. `%C` — хеш от локального хоста, целевого хоста, порта, пользователя и цепочки прыжков; документация рекомендует включать в `ControlPath` либо `%h`, `%p` и `%r`, либо `%C` вместо них. Хеш здесь практичнее: путь к unix-сокету ограничен примерно сотней символов, и на длинных именах хостов развёрнутый вариант в этот лимит не помещается.

`ControlPersist 10m` держит мастер-соединение живым десять минут после закрытия последней сессии — следующая команда к тому же хосту подключится мгновенно.

Эффект заметнее всего там, где соединений много и они короткие: `git` по SSH, `rsync` в цикле, Ansible (у него есть собственная настройка мультиплексирования по той же причине), любые скрипты с несколькими `ssh` подряд.

Грабли ровно одни — залипший сокет. Если сеть оборвалась, мастер может остаться в состоянии, когда он не работает и не умирает, и все новые подключения к этому хосту виснут. Управление явное:

```bash
ssh -O check prod-db      # жив ли мастер
ssh -O exit prod-db       # закрыть мастер
ssh -S none prod-db       # подключиться в обход мультиплексирования
```

Ещё одно следствие, о котором стоит знать заранее: пока мастер жив, изменения в `~/.ssh/config` для этого хоста не применяются — новые сессии наследуют параметры мастера.

## Ключи

### Что генерировать

```bash
ssh-keygen -t ed25519 -C "user@laptop-2026"
```

Ed25519 — разумное умолчание: ключи фиксированной короткой длины, быстрая проверка подписи, отсутствие вариантов «сделать неправильно». RSA остаётся нужен только для совместимости со старым оборудованием; если он всё же берётся, то не короче 4096 бит. DSA обсуждать не нужно вовсе: поддержка отключена по умолчанию с 2015 года, а в OpenSSH 10.0 (апрель 2025) удалена совсем.

Аппаратные токены поддерживаются нативно, без стороннего софта:

```bash
ssh-keygen -t ed25519-sk -O resident -O verify-required -C "yubikey-2026"
```

Приватная часть такого ключа не покидает токен: в файле лежит только «указатель», и без физического устройства он бесполезен. `verify-required` добавляет требование подтвердить личность PIN-кодом или биометрией, `resident` записывает ключ в память токена, откуда его можно выгрузить на новой машине (`ssh-keygen -K`). Это лучшая на сегодня защита от кражи ключа с ноутбука.

Отдельно — постквантовые подписи. В OpenSSH 10.4 (июль 2026) появился гибридный тип `mldsa44-ed25519`, сочетающий ML-DSA-44 с Ed25519. Он экспериментальный и по умолчанию выключен: чтобы им пользоваться, тип нужно явно добавить в `PubkeyAcceptedAlgorithms` и `HostKeyAlgorithms` с обеих сторон. Для обмена ключами постквантовая схема, наоборот, уже стала умолчанием — `mlkem768x25519-sha256` включён по умолчанию с OpenSSH 10.0, так что защита самого канала от «сохранить сейчас, расшифровать потом» работает без настройки. С 10.1 клиент вдобавок предупреждает, когда согласован не постквантовый обмен ключами — то есть когда сервер слишком старый; предупреждение управляется параметром `WarnWeakCrypto`, который по умолчанию включён, а конкретно это сообщение гасится флагом `no-pq-kex`.

### Парольная фраза и агент

Ключ без парольной фразы — это файл, дающий доступ ко всей инфраструктуре тому, кто получил ваш диск. Фраза ставится всегда, а чтобы не вводить её постоянно, ключ загружается в агент:

```bash
ssh-add -t 8h ~/.ssh/id_ed25519      # забыть ключ через 8 часов
ssh-add -l                            # что сейчас в агенте
ssh-add -D                            # выгрузить всё
```

Ограничение времени жизни — не формальность: агент хранит расшифрованный ключ в памяти процесса, и бессрочно загруженный ключ означает, что компрометация сессии в любой момент рабочего дня равна компрометации ключа. Рабочий день как срок жизни — разумный компромисс.

`AddKeysToAgent yes` в конфиге добавляет ключ в агент при первом использовании, так что явный `ssh-add` нужен редко.

## Доступ через бастион

### ProxyJump

Бастион (jump host) — единственная машина, доступная снаружи, через которую идёт доступ к остальным. Раньше это делалось через `ProxyCommand` с вложенным `nc`; с появлением `ProxyJump` конструкция свелась к одному параметру:

```bash
ssh -J bastion.example.com prod-db
ssh -J user1@bastion:2222,user2@relay prod-db   # цепочка из двух прыжков
```

В конфиге:

```conf
Host bastion
    HostName bastion.example.com
    User jump
    IdentityFile ~/.ssh/id_ed25519_bastion

Host prod-*
    ProxyJump bastion
    User deploy
    IdentityFile ~/.ssh/id_ed25519_prod
```

Принципиальное свойство: соединение с целевым хостом шифруется от вашей машины до цели, а бастион только пересылает байты. Он не видит содержимое сессии и не участвует в аутентификации на цели. Ключ для цели никогда не покидает вашу машину.

Прыжки нужны не только `ssh`. `scp` и `sftp` понимают `-J`, а `rsync` — через параметр удалённой оболочки:

```bash
rsync -avP -e 'ssh -J bastion' ./data/ prod-db:/srv/data/
```

Если хост описан в `~/.ssh/config`, отдельного флага не нужно вовсе — `rsync -avP ./data/ prod-db:/srv/data/` подхватит `ProxyJump` из конфига.

`ProxyCommand` остаётся нужен там, где транспорт не сводится к «подключиться по SSH к промежуточному хосту»: доступ через HTTP CONNECT-прокси, через `cloudflared`, через нестандартную утилиту. Для обычного бастиона он проигрывает `ProxyJump` во всём.

### Agent forwarding и почему его лучше не включать

`ForwardAgent yes` (флаг `-A`) пробрасывает сокет вашего агента на удалённый хост, чтобы оттуда подключаться дальше вашими ключами. Соблазн очевиден, опасность — нет.

Проброшенный сокет — это файл на удалённой машине. Любой, кто может обойти права доступа к нему — а root может всегда, — получает возможность обращаться к вашему агенту, пока сессия открыта. Ключ он не украдёт: агент не отдаёт приватную часть. Но он может попросить агент подписать что угодно, а этого достаточно, чтобы зайти под вами на любой хост, куда пускает этот ключ. Документация предупреждает об этом прямо, и по умолчанию `ForwardAgent` выключен. Одну грань риска в OpenSSH 10.1 подрезали: сокеты агента — и локальные, и проброшенные на сервер — переехали из `/tmp` в `~/.ssh/agent/`, так что процесс, которому виден только `/tmp`, до агента больше не дотягивается. От того, у кого на удалённой машине root, это не спасает.

Практический вывод простой: на бастионе, которым пользуется не только администратор, проброс агента означает доверие всем, у кого там root. Правильная замена — `ProxyJump`: он решает ту же задачу и никакого агента никуда не пробрасывает. Если проброс всё же необходим, ограничивайте его конкретным хостом (`Host bastion` + `ForwardAgent yes`, но не в `Host *`) и используйте агент с подтверждением каждой подписи — `ssh-add -c`, который заставляет подтверждать каждое использование ключа локально.

## Сертификаты вместо authorized_keys

### Что это решает

Схема с `authorized_keys` имеет два дефекта, которые проявляются с ростом парка машин.

Первый — **отзыв доступа**. Уволился сотрудник, и его ключ надо убрать со всех серверов. Пока серверов пять, это скрипт; когда их пятьдесят и часть выключена, гарантии, что ключ убран отовсюду, нет.

Второй — **known_hosts на стороне клиента**. Каждый новый сервер вызывает вопрос про отпечаток, на который все отвечают «yes» не глядя. Переустановка сервера ломает запись, и пользователь видит грозное предупреждение о возможной атаке, которое приучается игнорировать — вместе с настоящими предупреждениями.

Сертификаты решают обе задачи одним механизмом: стороны доверяют не множеству ключей, а одному ключу удостоверяющего центра, который эти ключи подписывает. Формат OpenSSH — собственный, заметно более простой, чем X.509.

### Свой CA

```bash
ssh-keygen -t ed25519 -f ssh_user_ca -C "user CA"
ssh-keygen -t ed25519 -f ssh_host_ca -C "host CA"
```

Два отдельных CA — не перестраховка, а разделение ролей: ключ, подписывающий пользователей, не должен уметь выдавать себя за сервер, и наоборот.

Пользовательский сертификат:

```bash
ssh-keygen -s ssh_user_ca -I "alice@2026-08" -n alice,deploy \
           -V +8h ~/.ssh/id_ed25519.pub
```

`-I` — идентификатор, попадающий в логи сервера и служащий единицей отзыва. `-n` — список принципалов, то есть учётных записей, под которыми этот сертификат разрешает входить. `-V +8h` — срок действия; результат кладётся рядом с ключом как `id_ed25519-cert.pub` и подхватывается автоматически.

Короткий срок действия — главная ценность схемы. Сертификат на восемь часов не нужно отзывать: он протухнет сам. Отзыв доступа сводится к тому, чтобы перестать выдавать новые.

На сервере доверие настраивается двумя строками:

```conf
# /etc/ssh/sshd_config.d/60-ca.conf
TrustedUserCAKeys /etc/ssh/ssh_user_ca.pub
RevokedKeys /etc/ssh/revoked_keys.krl
```

Важная деталь: сертификат без списка принципалов через `TrustedUserCAKeys` к аутентификации не допускается. Если нужно отвязать имена принципалов от имён учётных записей — например, пускать всех с принципалом `sre` под пользователя `deploy`, — это делается через `AuthorizedPrincipalsFile`.

### Хостовые сертификаты

Вторая половина схемы, которую часто пропускают, а зря — именно она убирает вопросы про отпечатки.

```bash
ssh-keygen -s ssh_host_ca -I "web01" -h -n web01.example.com,10.0.1.5 \
           -V +52w /etc/ssh/ssh_host_ed25519_key.pub
```

```conf
# /etc/ssh/sshd_config.d/60-ca.conf
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
```

На клиенте вместо десятков строк в `known_hosts` — одна:

```text
@cert-authority *.example.com ssh-ed25519 AAAAC3NzaC1lZDI1... host CA
```

Теперь любой сервер домена, предъявивший сертификат этого CA, принимается без вопросов, а переустановленный сервер с новым ключом и новым сертификатом не вызывает предупреждений. При этом настоящая подмена сервера по-прежнему обнаруживается: у злоумышленника нет подписи CA.

### Отзыв

Для случаев, когда ждать истечения срока нельзя, есть списки отзыва:

```bash
ssh-keygen -k -f revoked_keys.krl compromised_key.pub    # создать KRL
ssh-keygen -k -f revoked_keys.krl -u another_key.pub     # дополнить
ssh-keygen -Q -f revoked_keys.krl some_key.pub           # проверить
```

Файл, указанный в `RevokedKeys`, читается при каждой попытке аутентификации, поэтому заменять его надо атомарно (создать рядом и `mv`), а не править на месте. И отдельная мина: если файл станет нечитаемым, аутентификация по ключам будет отвергнута **для всех**.

### Когда это окупается

Честный ответ: не сразу. Инфраструктура CA — это защищённое хранение ключа удостоверяющего центра, процедура выдачи сертификатов и её автоматизация. Ради трёх личных серверов городить это бессмысленно — `authorized_keys` и один-два ключа проще во всём.

Ориентиры, при которых схема начинает выигрывать: машин больше двух-трёх десятков, людей с доступом больше пяти, серверы регулярно пересоздаются, или есть требование отзывать доступ гарантированно и быстро. Отдельный сильный аргумент — эфемерная инфраструктура: в автомасштабируемой группе новые хосты появляются постоянно, и раздавать на них ключи бессмысленно, а хостовой сертификат выдаётся при подготовке образа.

Промежуточный вариант, который часто оказывается достаточным: только **хостовые** сертификаты, без пользовательских. Это дёшево — один CA и подпись при развёртывании, — и снимает всю боль с `known_hosts`, оставив раздачу пользовательских ключей как была.

## known_hosts

Модель доверия здесь называется TOFU (trust on first use): первое подключение принимается на веру, дальше ключ запоминается и сверяется. Слабое место очевидно — если подменить сервер удалось при первом подключении, схема не сработает вовсе.

Три вещи, которые стоит знать про этот файл.

**Хеширование имён.** `HashKnownHosts` заменяет имена и адреса хешами, чтобы файл не выдавал список ваших серверов тому, кто получил доступ к домашнему каталогу. Здесь заметное расхождение между вендорами: upstream-умолчание — `no`, а Debian и Ubuntu включают его в `/etc/ssh/ssh_config`. Отсюда разное поведение на разных машинах и невозможность вручную найти строку `grep`-ом там, где хеширование включено. Преобразовать существующий файл можно через `ssh-keygen -H`, а найти запись в хешированном — через `ssh-keygen -F имя`.

```bash
ssh-keygen -F prod-db                  # найти запись (работает и с хешами)
ssh-keygen -R prod-db                  # удалить запись после переустановки
```

**SSHFP в DNS.** Отпечаток ключа можно опубликовать в DNS-записи и проверять по ней вместо TOFU:

```bash
ssh-keygen -r web01.example.com        # сгенерировать записи для зоны
```

```conf
VerifyHostKeyDNS yes
```

При `yes` клиент молча доверяет отпечатку, полученному из DNS **безопасно**, то есть при работающем DNSSEC; без него запись считается ненадёжной, и поведение откатывается к вопросу пользователю. Отсюда честная оценка: без DNSSEC схема не даёт настоящей гарантии, и в этом случае хостовые сертификаты — более практичный путь.

**UpdateHostKeys.** Современный клиент умеет принимать от сервера уведомления о дополнительных ключах и сам обновлять `known_hosts`. Это то, что позволяет серверу безболезненно перейти, например, с RSA на Ed25519: клиент узнает новый ключ заранее, по уже доверенному соединению.

## Туннели

Три флага, которые стоит различать чётко.

**`-L` — локальный проброс.** Порт на вашей машине ведёт к сервису, доступному с удалённого хоста. Классика — доступ к базе, слушающей только на localhost сервера:

```bash
ssh -L 5432:localhost:5432 -N prod-db
psql -h 127.0.0.1 -p 5432
```

Адрес `localhost` здесь разрешается **на удалённой стороне**, и это ровно то, что нужно. Через бастион к третьей машине: `ssh -J bastion -L 5432:10.0.4.12:5432 -N gateway`.

**`-R` — обратный проброс.** Порт на удалённом хосте ведёт к сервису на вашей машине. Основной инструмент для доступа к машине без белого адреса:

```bash
ssh -R 8080:localhost:80 -N vps
```

По умолчанию проброшенный порт слушает только на loopback удалённой машины. Чтобы он был доступен извне, на сервере нужен `GatewayPorts yes` или `clientspecified` — и это решение с последствиями: открытый таким образом порт доступен всему интернету, без какой-либо аутентификации со стороны SSH. `clientspecified` безопаснее, потому что оставляет выбор адреса привязки клиенту (`-R 127.0.0.1:8080:...`).

Сценарий публикации домашнего сервиса через арендованный VPS разобран отдельно — там же про то, почему для постоянной схемы обратный туннель SSH проигрывает WireGuard: см. [самохостинг за NAT](./selfhosting-behind-nat.md).

**`-D` — SOCKS-прокси.** Динамический проброс: SSH поднимает локальный SOCKS5-прокси, и приложения, умеющие в него ходить, получают выход в сеть с удалённой машины.

```bash
ssh -D 1080 -N bastion
curl --socks5-hostname 127.0.0.1:1080 http://internal-app.corp/
```

Форма `--socks5-hostname` существенна: она заставляет разрешать имена на удалённой стороне. С обычным `--socks5` имя резолвится локально, внутренние имена не разрешаются, а ваши DNS-запросы утекают наружу.

Флаг `-N` во всех примерах означает «не выполнять команду», а `-f` уводит процесс в фон. Для долгоживущих туннелей лучше не `-f`, а юнит systemd с `Restart=always`: голый `ssh` умирает вместе с сетью и сам не возвращается.

## Серверная сторона

### sshd_config.d и порядок включения

Начиная с OpenSSH 8.2 (февраль 2020) `sshd_config` поддерживает директиву `Include`, и это меняет подход к правкам. Дистрибутивы семейства Debian ставят её в начало основного файла:

```conf
Include /etc/ssh/sshd_config.d/*.conf
```

Смысл — не в эстетике. Правки, положенные отдельным файлом, переживают обновление пакета: менеджер пакетов не трогает ваш файл в `sshd_config.d/` и не задаёт вопросов о конфликте с новой версией основного конфига. При правке основного файла вы получаете либо диалог «оставить своё или взять новое», либо, что хуже, тихую потерю настроек.

Здесь же ловушка, вытекающая из правила первого значения: **`Include` обязан стоять в начале файла**. Если поставить его в конец, все параметры уже будут заданы основным конфигом, и файлы из `sshd_config.d/` не переопределят ничего. Файлы по глобу читаются в лексическом порядке, поэтому их принято нумеровать: `10-`, `50-`, `99-`.

```bash
# /etc/ssh/sshd_config.d/50-hardening.conf
PermitRootLogin no
PasswordAuthentication no
```

### Match: исключение из правила

`Match` в `sshd_config` работает не так, как остальные директивы, и это стоит держать в голове. Параметры внутри сработавшего блока **переопределяют** глобальные, а не проигрывают им по правилу первого значения. При этом, если один и тот же параметр встречается в нескольких сработавших блоках `Match`, применяется первый.

Всё, что идёт после строки `Match`, относится к этому блоку до следующего `Match` или до конца файла. Отступы ни на что не влияют — это частая ошибка при чтении чужого конфига: визуально «глобальный» параметр в конце файла на самом деле принадлежит последнему блоку.

```conf
# глобально — строго
PasswordAuthentication no
AllowTcpForwarding no
X11Forwarding no

Match Group developers
    AllowTcpForwarding yes          # разработчикам нужны туннели

Match User backup, Address 10.0.0.0/8
    ForceCommand /usr/local/bin/backup-only
    PermitTTY no
```

`ForceCommand` выполняет заданную команду вместо всего, что запросил клиент, — оригинальный запрос при этом доступен в переменной `SSH_ORIGINAL_COMMAND`. Это основа для ограниченных учётных записей: резервное копирование, приём git-репозиториев, единственная разрешённая операция.

### Ограниченные ключи

Ограничения можно навесить и на конкретный ключ, без правки серверного конфига — префиксом перед ключом в `authorized_keys`:

```text
restrict,from="10.0.0.0/8",command="/usr/local/bin/deploy.sh" ssh-ed25519 AAAAC3... deploy-bot
```

`restrict` — правильная отправная точка. Он отключает всё сразу: проброс портов, агента, X11, выделение терминала и выполнение `~/.ssh/rc`. Ключевое его свойство описано в документации явно: **новые виды ограничений, которые появятся в будущих версиях, автоматически попадают в этот набор**. Старый способ — перечислять `no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty` — оставляет дыру ровно в момент, когда в OpenSSH добавится новая возможность, о которой ваш конфиг не знает.

Нужное включается обратно поимённо: `restrict,pty` даёт терминал, оставив остальное закрытым. `from=` привязывает ключ к адресам, `command=` — к единственной команде.

### Как менять конфигурацию, не теряя доступ

Перезапуск `sshd` с ошибкой в конфиге на удалённой машине без консоли в панели провайдера — это конец рабочего дня. Дисциплина проста и стоит тридцати секунд:

```bash
sshd -t                      # проверить синтаксис
sshd -T                      # вывести итоговую конфигурацию целиком
sshd -T -C user=deploy,host=web01,addr=10.0.1.5   # с учётом Match-блоков
```

`sshd -T` особенно полезен при разборе, почему настройка «не применилась»: он показывает не то, что написано в файлах, а то, что получилось после всех включений и правила первого значения.

Вторая половина дисциплины — **не закрывать текущую сессию**, пока новая не проверена:

```bash
systemctl reload sshd        # reload, а не restart: существующие сессии живут
# в ДРУГОМ терминале, не закрывая текущий:
ssh -v user@host             # проверить, что вход работает
```

`reload` предпочтительнее `restart` именно потому, что не рвёт установленные соединения. Если новый конфиг сломал вход, у вас остаётся открытая сессия, чтобы откатить правку.

## Отладка

Первое средство — подробный вывод клиента. Уровни различаются: `-v` показывает ход аутентификации, `-vv` добавляет обмен параметрами, `-vvv` — пакеты.

```bash
ssh -v prod-db 2>&1 | grep -i "offering\|authentications that can continue\|Server accepts"
```

Что искать в выводе: строку `Authentications that can continue` (какие методы сервер вообще принимает), последовательность `Offering public key` (какие ключи предлагает клиент и в каком порядке), `Server accepts key` (сервер согласился на ключ, дальше идёт подпись).

Начиная с OpenSSH 10.5 для того же вопроса есть отдельный режим: `ssh -Z user@host` печатает ключи, которые будут предложены, в порядке использования, и завершается, не подключаясь к серверу. При разборе `Too many authentication failures` это быстрее, чем читать `-v`.

| Симптом | Обычная причина | Куда смотреть |
|---------|-----------------|---------------|
| `Too many authentication failures` | клиент перебрал чужие ключи до нужного | `IdentitiesOnly yes`, `ssh-add -D` |
| `Permission denied (publickey)` | ключа нет в `authorized_keys` либо не тот пользователь | `-v`: предлагался ли нужный ключ; логи `sshd` |
| Ключ верный, но вход не проходит | права на `~/.ssh` или `authorized_keys` | `chmod 700 ~/.ssh`, `600 authorized_keys`; владелец каталога |
| Пароль спрашивается вместо ключа | `PubkeyAuthentication no` или ключ не предложен | `sshd -T \| grep -i pubkey` |
| Правка конфига «не работает» | сработало первое значение выше по файлу | `ssh -G host`, `sshd -T` |
| Зависает после установления TCP | залипший мастер `ControlMaster` | `ssh -O check`, `ssh -S none` |
| `REMOTE HOST IDENTIFICATION HAS CHANGED` | сервер переустановлен либо подмена | `ssh-keygen -R host` после проверки отпечатка |

Симметричная команда для клиентской стороны — `ssh -G host`: она печатает итоговую конфигурацию для конкретного хоста после применения всех блоков, никуда не подключаясь. При разборе «почему берётся не тот ключ» это быстрее чтения конфига глазами.

Серверная сторона отлаживается через журнал (`journalctl -u ssh -f`) либо, если причина непонятна, запуском второго `sshd` на другом порту в отладочном режиме:

```bash
/usr/sbin/sshd -d -p 2222        # не трогая основной демон
ssh -p 2222 user@host
```

## Сравнение с FreeBSD

OpenSSH входит в базовую систему FreeBSD, а не ставится пакетом, и отсюда несколько отличий.

Версия в базе отстаёт от upstream: и во FreeBSD 14, и во FreeBSD 15 это OpenSSH 10.0 при 10.5 в текущем релизе проекта. Разрыв обычно не критичен, но возможности последних версий там появляются с задержкой, и это стоит проверять перед тем, как закладываться на свежий параметр.

Директивы `Include` в поставляемом `/etc/ssh/sshd_config` нет — каталог `sshd_config.d/` во FreeBSD не заведён по умолчанию, в отличие от Debian и производных. Сам механизм поддерживается, так что строку можно добавить самостоятельно, но из коробки правки идут в основной файл, и после `freebsd-update` его придётся сверять вручную.

Есть и специфичные для системы параметры. `UseBlocklist yes` включает интеграцию с `blocklistd` — штатным механизмом блокировки после неудачных попыток входа, который заменяет собой fail2ban; подробности в [заметке про hardening FreeBSD](./freebsd-security-hardening.md). Имя сменилось в FreeBSD 15: в 14.x демон и параметр по-прежнему зовутся `blacklistd` и `UseBlacklist`, а sshd принимает оба написания как алиасы, так что конфиг переживает переход между ветками. `VersionAddendum` управляет строкой с меткой FreeBSD в баннере версии.

Если OpenSSH из базы не устраивает, в портах есть `security/openssh-portable` — свежая версия, устанавливаемая параллельно, со своим конфигом в `/usr/local/etc/ssh/` и отдельным rc-скриптом. Держать оба демона одновременно на одном порту нельзя, и переключение требует аккуратности: отключить базовый `sshd` в `rc.conf` и включить портовый, не потеряв доступ.

## Источники

- [ssh_config(5)](https://man.openbsd.org/ssh_config) — правило первого значения, `Match`, `ControlMaster`/`ControlPath` и токены, `ProxyJump`, `IdentitiesOnly`, `HashKnownHosts`, `VerifyHostKeyDNS`
- [sshd_config(5)](https://man.openbsd.org/sshd_config) — `Include`, семантика `Match` как переопределения, `TrustedUserCAKeys`, `AuthorizedPrincipalsFile`, `RevokedKeys`, `HostCertificate`
- [sshd(8), раздел AUTHORIZED_KEYS](https://man.openbsd.org/sshd) — `restrict` и его свойство включать будущие ограничения
- [ssh-keygen(1), разделы CERTIFICATES и KEY REVOCATION LISTS](https://man.openbsd.org/ssh-keygen) — подписание ключей, `-I`, `-n`, `-V`, генерация KRL
- [OpenSSH — release notes](https://www.openssh.com/releasenotes.html): 8.2 (`Include` в sshd_config), 9.8 (выделение `sshd-session` из `sshd`), 10.0 (удаление DSA, `mlkem768x25519-sha256` по умолчанию, вынос аутентификации в отдельный `sshd-auth`), 10.1 (`WarnWeakCrypto`, сокеты агента в `~/.ssh/agent`), 10.4 (гибридные подписи `mldsa44-ed25519`), 10.5 (`ssh -Z`)
- [Дерево исходников OpenSSH](https://github.com/openssh/openssh-portable) — текущая версия 10.5p1 по `version.h`
- [Дерево исходников FreeBSD](https://github.com/freebsd/freebsd-src/tree/stable/14/crypto/openssh) — версия OpenSSH в базе и поставляемый `sshd_config` без `Include`
- [Debian: openssh-client, отличия умолчаний](https://manpages.debian.org/stable/openssh-client/ssh_config.5.en.html) — `HashKnownHosts yes` и другие параметры, заданные дистрибутивом

---

## Связанные заметки

- [Linux — безопасность: hardening, SELinux/AppArmor](./linux-security-hardening.md) — базовый набор настроек `sshd_config` и защита от перебора
- [FreeBSD — безопасность](./freebsd-security-hardening.md) — то же со стороны FreeBSD, `blacklistd` и PF overload tables
- [Самохостинг за NAT: VPS как точка входа](./selfhosting-behind-nat.md) — обратные туннели `-R` и почему для постоянной схемы берут WireGuard
- [Linux — сеть: стек, конфигурация, firewall](./linux-networking.md) — что происходит уровнем ниже туннелей
- [Linux — пользователи, группы, PAM и systemd-logind](./linux-users-pam.md) — стек `/etc/pam.d/sshd`, который вызывает sshd после проверки ключа
- [AEAD: аутентифицированное шифрование, nonce и лимиты](./crypto-aead.md) — что происходит с трафиком после обмена ключами и почему Terrapin оказался провалом протокола, а не шифра
- [ChaCha20 и Poly1305: устройство связки и почему она такая](./crypto-chacha20-poly1305.md) — как устроен `chacha20-poly1305@openssh.com`, зачем ему два экземпляра шифра и что стандартизует черновик sshm
- [Обмен ключами: X25519, KEM, вывод ключей и привязка к транскрипту](./crypto-key-exchange.md) — что стоит за `mlkem768x25519-sha256` и `mldsa44-ed25519`, exchange hash и вывод ключей по буквам
- [AES-GCM против ChaCha20-Poly1305: как выбирать](./crypto-aes-vs-chacha20.md) — что означает умолчание `Ciphers` в OpenSSH и когда клиенту стоит поднять AES-GCM явно
