Bastion host: безопасный SSH-доступ к закрытой инфраструктуре

Настройка сервера-прыжка (jump server): hardening SSH на бастионе, ProxyJump в конфиге клиента, SSH agent forwarding и ограничение доступа к внутренним серверам по IP.

Внутренние серверы — базы данных, мониторинг, очереди — не должны быть напрямую доступны по SSH из интернета. Вместо этого используют один публично открытый сервер (bastion), через который выполняются все SSH-подключения к закрытой инфраструктуре. Такой подход сокращает поверхность атаки: 22-й порт закрыт на всех серверах, кроме одного, и аудит SSH-сессий централизован.

Проверено на: Ubuntu 24.04 LTS, OpenSSH 9.x
Уровень сложности: средний
Время выполнения: 20–30 минут
Требуемый доступ: root или sudo на бастионе и внутреннем сервере

Что будет настроено

  • hardening SSH-демона на бастионе под роль jump server;
  • SSH agent forwarding — закрытый ключ не хранится на бастионе;
  • ~/.ssh/config с ProxyJump на клиентской машине;
  • ограничение SSH-доступа к внутреннему серверу только с IP бастиона через UFW;
  • удобные алиасы для подключения одной командой.

Исходные условия

  • два сервера Ubuntu 24.04: бастион с публичным IP и хотя бы один внутренний сервер;
  • SSH-ключ уже сгенерирован на клиентской машине, публичная часть скопирована на оба сервера;
  • UFW активирован на обоих серверах.

В примерах используются плейсхолдеры:

BASTION_IP      — публичный IP бастиона, например 198.51.100.10
INTERNAL_IP     — IP внутреннего сервера, например 10.0.0.5
ADMIN_USER      — ваш пользователь-администратор

Схема подключения

  Клиент
  (ноутбук / рабочая машина)
       │
       │  SSH (порт 22, публичная сеть)
       ▼
  ┌──────────────────┐
  │   Bastion host   │  ← единственный сервер с открытым SSH наружу
  │  BASTION_IP      │
  └──────────────────┘
       │
       │  SSH (порт 22, только с BASTION_IP)
       ▼
  ┌──────────────────┐
  │  Internal server │  ← порт 22 закрыт для всех, кроме бастиона
  │  INTERNAL_IP     │
  └──────────────────┘

Приватный ключ клиента не копируется на бастион — он остаётся на клиентской машине и передаётся через SSH agent forwarding.

Hardening SSH на бастионе

Откройте конфигурационный файл SSH-демона:

cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
nano /etc/ssh/sshd_config

Найдите и приведите к следующему виду (или добавьте в конец файла):

cat >>/etc/ssh/sshd_config.d/bastion.conf <<'EOF'
# Bastion role configuration
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
X11Forwarding no
PermitTunnel no
AllowUsers ADMIN_USER

# AllowTcpForwarding must be yes for ProxyJump to function
AllowTcpForwarding yes
EOF

Параметры означают:

  • PermitRootLogin no — вход под root запрещён;
  • PasswordAuthentication no — только ключевая аутентификация;
  • MaxAuthTries 3 — три попытки ввода ключа, затем разрыв;
  • AllowUsers — SSH разрешён только указанному пользователю;
  • X11Forwarding no и PermitTunnel no — отключает неиспользуемые функции;
  • AllowTcpForwarding yes — обязателен для работы ProxyJump.

Проверьте конфигурацию и перезапустите демон:

sshd -t
systemctl restart ssh
systemctl is-active ssh

Важно: не закрывайте текущую SSH-сессию. Откройте второй терминал и убедитесь, что подключение к бастиону работает.

Проверка ключа на серверах

Убедитесь, что публичный ключ присутствует на обоих серверах:

# На бастионе
cat ~/.ssh/authorized_keys

# На внутреннем сервере
cat ~/.ssh/authorized_keys

Если ключ ещё не скопирован на внутренний сервер, скопируйте его с клиентской машины:

# Выполняется на клиентской машине
ssh-copy-id -i ~/.ssh/id_ed25519.pub ADMIN_USER@INTERNAL_IP

SSH agent на клиентской машине

Приватный ключ должен быть загружен в SSH agent — только тогда он будет доступен для forwarding при прыжке через бастион.

Запустите агент и добавьте ключ:

# Запуск агента (если не запущен)
eval "$(ssh-agent -s)"

# Добавление ключа
ssh-add ~/.ssh/id_ed25519

# Проверка загруженных ключей
ssh-add -l

Ожидаемый вывод:

256 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ADMIN_USER@hostname (ED25519)

Чтобы агент запускался автоматически, добавьте в ~/.bashrc или ~/.zshrc:

cat >>~/.bashrc <<'EOF'

# SSH agent autostart
if [ -z "$SSH_AUTH_SOCK" ]; then
  eval "$(ssh-agent -s)" >/dev/null
  ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi
EOF

Настройка ~/.ssh/config на клиенте

Создайте или дополните файл конфигурации SSH на клиентской машине:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/config

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

# ─── Bastion host ────────────────────────────────────────
Host bastion
  HostName BASTION_IP
  User ADMIN_USER
  IdentityFile ~/.ssh/id_ed25519
  ForwardAgent yes
  ServerAliveInterval 60
  ServerAliveCountMax 3

# ─── Internal servers via ProxyJump ──────────────────────
Host internal-1
  HostName INTERNAL_IP
  User ADMIN_USER
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump bastion
  ServerAliveInterval 60
  ServerAliveCountMax 3

Параметры означают:

  • ForwardAgent yes — передаёт SSH agent через бастион, ключ не хранится на нём;
  • ProxyJump bastion — прыжок через хост bastion, описанный выше в том же файле;
  • ServerAliveInterval и ServerAliveCountMax — предотвращают разрыв сессии при простое.

Установите корректные права на файл:

chmod 600 ~/.ssh/config

Ограничение доступа к внутреннему серверу

На внутреннем сервере разрешите SSH только с IP бастиона:

# Сначала убедитесь, что правило для SSH с любого адреса не активно
sudo ufw status numbered

Если есть открытое правило 22/tcp без ограничения источника — удалите его. Номер строки возьмите из вывода предыдущей команды:

sudo ufw delete NUM

Добавьте разрешение только с бастиона:

sudo ufw allow from BASTION_IP to any port 22 proto tcp \
  comment 'SSH from bastion only'

Проверьте результат:

sudo ufw status verbose

Ожидаемый результат:

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    BASTION_IP

Предупреждение: после применения этого правила прямое подключение ssh ADMIN_USER@INTERNAL_IP с любого адреса, кроме бастиона, будет отклонено. Проверьте, что соединение через бастион работает, прежде чем закрывать текущую сессию.

Проверка соединения

Подключитесь к бастиону:

ssh bastion

С бастиона убедитесь, что SSH agent передался — ключ виден без копирования на бастион:

ssh-add -l

Ожидаемый вывод:

256 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ADMIN_USER@hostname (ED25519)

Выйдите с бастиона и подключитесь к внутреннему серверу через ProxyJump одной командой с клиентской машины:

ssh internal-1

Проверьте, что подключение прошло через бастион:

# На внутреннем сервере
who
last -10

В выводе будет указан IP бастиона, а не IP клиентской машины:

ADMIN_USER   pts/0        BASTION_IP   Mon Jan 01 12:00

Подключение без конфига

Если ~/.ssh/config недоступен, используйте флаг -J напрямую:

ssh -J ADMIN_USER@BASTION_IP ADMIN_USER@INTERNAL_IP

Флаг -A включает agent forwarding явно:

ssh -A -J ADMIN_USER@BASTION_IP ADMIN_USER@INTERNAL_IP

Копирование файлов через бастион:

# С клиента на внутренний сервер
scp -o "ProxyJump ADMIN_USER@BASTION_IP" \
  file.txt ADMIN_USER@INTERNAL_IP:~/

# Или через конфиг (короче)
scp file.txt internal-1:~/

Несколько внутренних серверов

При расширении инфраструктуры добавляйте записи в ~/.ssh/config:

Host internal-2
  HostName 10.0.0.6
  User ADMIN_USER
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump bastion

Host internal-3
  HostName 10.0.0.7
  User ADMIN_USER
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump bastion

На каждом внутреннем сервере повторите правило UFW:

sudo ufw allow from BASTION_IP to any port 22 proto tcp \
  comment 'SSH from bastion only'

Финальная проверка

# Подключение к бастиону
ssh bastion

# Agent forwarding работает на бастионе
ssh-add -l

# Прыжок на внутренний сервер с клиента
ssh internal-1

# Прямое подключение к внутреннему серверу отклонено
ssh -o ConnectTimeout=5 ADMIN_USER@INTERNAL_IP

# Правило UFW на внутреннем сервере
sudo ufw status verbose | grep 22

# sshd на бастионе принимает только нужного пользователя
sudo sshd -T | grep -E 'allowusers|permitrootlogin|passwordauth|allowtcpforward'

Ожидаемые результаты:

ssh ADMIN_USER@INTERNAL_IP   →  ssh: connect to host ... Connection timed out
ufw status                   →  22/tcp ALLOW IN BASTION_IP
sshd -T                      →  allowusers ADMIN_USER
                                 permitrootlogin no
                                 passwordauthentication no
                                 allowtcpforwarding yes

Что не охватывает эта инструкция

Это базовая конфигурация jump server. В production дополнительно стоит рассмотреть:

  • нестандартный порт SSH на бастионе;
  • Fail2ban на бастионе для блокировки перебора ключей;
  • ограничение SSH на бастионе по IP клиента через UFW;
  • аудит SSH-сессий через auditd или tlog;
  • время жизни сессии через ClientAliveInterval и ClientAliveCountMax на бастионе;
  • сертификаты SSH CA вместо распределения authorized_keys по серверам;
  • Teleport или аналоги — специализированные решения класса PAM для больших инфраструктур.

Итог

После выполнения инструкции получается:

  • единственная публично открытая точка SSH-входа — бастион;
  • внутренние серверы принимают SSH только с IP бастиона;
  • приватный ключ не покидает клиентскую машину благодаря agent forwarding;
  • подключение к любому внутреннему серверу — одна команда ssh internal-1;
  • поверхность атаки на SSH сведена к одному серверу с минимальными правами.
← Предыдущая статья Резервное копирование одной базы PostgreSQL через pg_dump