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 сведена к одному серверу с минимальными правами.