Об ssh

2018-10-14

Об эс-эс-эйч. Которая SSH. Которая Secure SHell. Не все, оказывается, толком представляют, что это такое. И уж тем более не подозревают обо всей мощи SSH.

Впервые я узнал об этой аббревиатуре, "SSH", из журнала "Byte" из 1990-х. В рамках курса английского в универе мы переводили эти исторические статьи. И вот в одной из них встретилась фраза вроде "he sshed to the remote server". Да, "to ssh" — это ещё и глагол.

SSH

SSH появился в 1995 усилиями ещё одного талантливого финна. Его звали Tatu Ylönen.

SSH — это сетевой протокол, работающий поверх TCP. Соответственно, у нас есть сервер, демон sshd, который слушает порт 22, и клиент ssh. У 22 порта есть своя история. Интернет тогда был маленький, и застолбить свой красивый привилегированный порт тогда было довольно просто.

Первая буква "S" в "SSH" действительно означает "Security". SSH возник именно как замена rlogin, rsh и telnet. Все они тоже позволяли подключиться к удалённому серверу и выполнять там команды, но весь трафик там передавался открытым текстом. А SSH — шифрует (и сжимает).

SSH возник примерно в одно время с первыми вариантами SSL (позднее известный как TLS). Поэтому они используют несколько разные подходы. Как минимум, SSH — это не SSL/TLS. А SSL/TLS — это универсальный способ добавить "S" в кучу других протоколов поверх TCP. Самый известный вариант: HTTPS.

В SSL/TLS публичный ключ сервера (и, опциально, клиента) представлен в виде сертификата, подписанного третьей доверенной стороной. И клиент доверяет серверу, если он доверяет этой третьей стороне. В SSH клиент доверяет каждому серверу индивидуально. При первом подключении пользователю явно задаётся вопрос: а доверяем ли мы этому хосту?

$ ssh root@host.example.net
The authenticity of host 'host.example.net (1.2.3.4)' can't be established.
ECDSA key fingerprint is SHA256:Xocqb7ZPtfsEmXnTCUOeRaCRMfzH1nbTxDVKp0YJBCA.
Are you sure you want to continue connecting (yes/no)?

Если ответить yes, то отпечаток ключа хоста сохранится в ~/.ssh/known_hosts. И при повторном подключении, если ключ сервера не изменился, вопросов уже не будет. А если изменился, то подключения не будет, потому что это уже другой сервер, если у него другой ключ. Простая и эффективная схема доверия.

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

$ ssh-keygen -R "host.example.net"
# Host host.example.net found: line 497
/home/gelin/.ssh/known_hosts updated.
Original contents retained as /home/gelin/.ssh/known_hosts.old

В SSL/TLS клиент, чаще всего, анонимен. Хотя и может предъявить свой сертификат и ключ. В SSH — никакой анонимности. Клиент обязан сказать, от имени какого пользователя (локального пользователя данного сервера) он собирается действовать, предъявить ключ, а там, возможно, ещё и ввести пароль этого самого пользователя.

В простейшем случае мы подключаемся от root и вводим пароль root.

$ ssh root@host.example.net
root@1.2.3.4's password:

Конечно, не надо так. Подключаться прямо к root, конечно, удобнее. Но подход Ubuntu, когда у root вообще нет пароля, и им вообще нельзя войти в систему, немного безопаснее. Сначала надо подключиться/войти под каким-нибудь "ubuntu". А потом всякие гадости можно делать через sudo. При этом понадобится ввести пароль этого самого "ubuntu".

Ну и, конечно же, не надо подключаться через SSH по паролю. Это ещё хуже, чем прямой root доступ. Потому что пароль можно подобрать. А 4096-битный RSA ключ, ну, почти нельзя. SSH предоставляет несколько методов аутентификации. Кроме "password" есть ещё весьма удобный и вполне безопасный "publickey".

У клиента всегда есть ключ. Без ключа нельзя подключиться. Публичный ключ клиента можно увидеть в файле ~/.ssh/id_rsa.pub.

$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcwgyvek9Oh5wUNbNUTiEtz1jeRn6WnF1mw3duhPC7B3N9HAvCVtYesu5xLy6wylcKYUdKPLo8Q6hEh8kTcHwL21xPQB4/Uq/PjLUvt+MS5mUfYKwWL/M9h37nztA/scK6ItYWMxP6hsX1/zhOkcw1VsLD0+tHRYVqmAm+qO2VQxJ4Gc0dJWeHIGPq1gLLLlJx1QJHUMbHFewtH8z18ood3w/Q07QIKy2kMxFTK/y6Kv1Ij0rxO1KnnzulJHOiNIffeec7nvjcwe0nLYW7y28sT9cxCBIxNu2Dzir8pqgM1TB+mAw/nsYft9CGYBWc+AaodERHXPIbm2wwUUFWnEAN gelin@zenbook

Файл не обязательно будет называться id_rsa.pub. Например, сейчас модно переходить на криптографию на эллиптических кривых. Тогда файлик будет называться id_ed25519.pub. Главное, смотрите файлики, чьё имя заканчивается на .pub. Это — публичные ключи. А без .pub — это приватные ключи. Берегите приватные ключи как зеницу ока и никому их не давайте даже под страхом смерти. И задавайте хороший пароль при генерации ключей.

$ ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519 -C "gelin@zenbook"
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/gelin/.ssh/id_ed25519.
Your public key has been saved in /home/gelin/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:FOjHEnorwa9eAb4Jhnqeh4rZ5zzsG/S8LzkBBm+DVCc gelin@zenbook
The key's randomart image is:
+--[ED25519 256]--+
|   .E ...        |
|  o  oo  .       |
| . =.o o.        |
| ...X.+.o        |
|. ooo*.+S        |
|.. o.=+.         |
|. .o+o+o         |
|.=.o*o+.         |
|+.+**o.+.        |
+----[SHA256]-----+

А публичные ключи можно раздавать налево и направо. Что я тут и делаю :) Всё равно, всё, что вы сможете с ними сделать, это дать мне доступ к вашим серверам :)

Чтобы аутентификация по публичному ключу заработала, ключ клиента (публичный!) нужно добавить в файл ~/.ssh/authorized_keys на сервере. В домашнем каталоге того пользователя, под кем мы подключаемся. Соответственно, домашний каталог у этого пользователя должен быть.

Добавить ключ в authorized_keys можно либо руками. Просто дописываете содержимое id_rsa.pub (одну строчку) в конец этого файла. Не забудьте пустую строку в конце. Либо вам может помочь команда ssh-copy-id.

$ ssh-copy-id -i ~/.ssh/id_ed25519 root@host.example.net
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/gelin/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'root@host.example.net'"
and check to make sure that only the key(s) you wanted were added.

Но мы же хотели ещё запретить подключение с паролем и прямой root доступ. Надо настроить sshd демона. Обычно его конфигурация лежит в файлике /etc/ssh/sshd_config. Нас интересуют несколько параметров.

PermitRootLogin no
PubkeyAuthentication yes    # не путать с RSAAuthentication
PasswordAuthentication no

После изменений в sshd_config делайте service ssh reload, не restart. И тренируйтесь в переподключении через SSH в другой консоли. Запускайте ssh с ключиком -v, чтобы видеть, как клиент с сервером договариваются. А эту сессию, где вы конфиг правили, не закрывайте. Тогда ваше подключение останется живым, даже если вы отломали SSH доступ. Крайне неприятно терять связь с удалённым сервером, знаете ли.

У клиента тоже есть конфиг. В файле ~/.ssh/config. Туда очень удобно прописывать имена юзеров и нестандартные порты подключения к конкретным серверам. Или даже какие-то особые SSH ключи, раз уж их вам выдали.

Host host.example.net
User root
Port 2222
IdentityFile ~/.ssh/example_net_key

Как вы знаете, Git и Mercurial тоже работают через SSH. Вот такой хитрой конфигурацией можно ходить на какой-нибудь Bitbucket из-под разных аккаунтов (и с разными ключами):

Host bitbucket.org
# обычный bitbucket, который использует ключ id_rsa

Host work.bitbucket.org
# отдельный bitbucket с отдельным ключом
HostName bitbucket.org
# но на самом деле мы всё равно подключаемся к bitbucket.org
User git
# юзером git
IdentityFile ~/.ssh/work@bitbucket.org
# и используем этот специальный ключ
IdentitiesOnly yes
# и только его

~/.ssh/config разбит на секции по директивам Host. Host — это то, что вы пишите в командной строке.

$ git clone git@work.bitbucket.org:user/repo.git

Это не обязательно должен быть реально существующий хост, настоящий адрес указывается в HostName. Все последующие директивы относятся в предыдущему (последнему) Host.

По умолчанию SSH запускает shell пользователя на сервере. Но на самом деле SSH может выполнить совершенно любую команду на удалённом хосте. И stdin/stdout на клиенте станут stdin/stdout этой удалённой команды. В своё время мы так собирали статистику с роутеров, запуская удалённый скрипт на Perl, который плевался XML.

$ ssh root@host.example.net whoami
root

Через SSH можно передавать файлы. Это значительно безопаснее, чем какой-нибудь FTP, куда шифрование толком так и не прикрутили. И это даже может быть быстрее.

Простой способ: scp. Secure CoPy. Можно скопировать локальный файл на удалённый сервер, или наоборот.

$ scp ssh.md user@host.example.net:/tmp/
ssh.md                  100%   13KB  74.6KB/s   00:00

Параметр, указывающий файл или каталог на удалённом хосте, состоит из двух частей. До двоеточия: имя пользователя и хост, так же как в обычном SSH клиенте. После двоеточия: абсолютный или относительный (относительно домашнего каталога) путь на сервере.

Но scp — туп. Он всегда копирует файл целиком. А вот если файл большой, или файлов много, нужно использовать rsync.

rsync — умный. Он умеет делить файлы на куски и проверять каждый кусок на идентичность, и докачивать только недостающие или отличающиеся куски. Он может сжимать файлы перед отправкой. Он может, как оно следует из названия, синхронизировать целые каталоги. С его помощью легко можно почти полностью склонировать почти любую Unix систему. Нужно только SSH на удалённый сервер, и установленный там rsync.

$ rsync -avz --progress . user@host.example.net:/tmp/test
sending incremental file list
created directory /tmp/test
./
00 URIs.png
        100,554 100%    4.31MB/s    0:00:00 (xfr#1, to-chk=2/4)
01 URI & URL
         38,506 100%    1.84MB/s    0:00:00 (xfr#2, to-chk=1/4)
url.md
          8,761 100%  371.99kB/s    0:00:00 (xfr#3, to-chk=0/4)

sent 139,314 bytes  received 108 bytes  39,834.86 bytes/sec
total size is 147,821  speedup is 1.06

Будьте осторожнее с параметрами rsync. Он по-разному работает с файлами и каталогами. Если вам нужно синхронизировать содержимое двух уже существующих каталогов, и не нужно создавать новые подкаталоги там, куда вы синхронизируете, оба параметра-каталога должны заканчиваться на /.

Кстати, rsync прекрасно работает и без SSH, на localhost.

Усложняем. Допустим, у нас есть какой-то Continuous Integration, который собирает некий статический сайт. И мы хотим, чтобы по окончанию сборки обновлённые файлы сами заливались бы на сервер. rsync для этого идеально подходит. Но мы не настолько доверяем нашему CI, чтобы давать ему root доступ. Заводим отдельного пользователя для синхронизации. Но хочется этого пользователя ещё сильнее ограничить, чтобы он мог делать только rsync только конкретного каталога. (Ну а в моём случае просто не было возможности завести ещё одного пользователя, ибо это shared hosting.) И в SSH это можно.

В файле authorized_keys перед ключом можно указать ещё и команду, которая будет разрешена к выполнению клиенту, предъявившему этот ключ. И больше никакая другая. И ещё кучу опций. А ещё есть специальный скрипт на Perl, под названием rrsync, который ограничивает доступ rsync к одному каталогу.

command="$HOME/bin/rrsync ~/limited/path/",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa ...

Как видите, SSH много чего может. И это много чего тут явно запрещается.

Примерно так, через хитрые command для каждого ключа, и работают публичные Git репозитории вроде GitHub или GitLab. Хотите поднять что-то подобное (ограничиваясь только Git, безо всяких трекеров задач) у себя локально, смотрите в сторону gitolite.

У SSH есть агент. Он может хранить в памяти расшифрованные ключи, чтобы вам не приходилось вводить пароль для ключа при каждом подключении. А ещё он может пробрасывать загруженные на клиенте ключи так, чтобы они были доступны на сервере (это и называется agent forwarding). Это очень удобно, если вам нужно с сервера делать SSH на следующий сервер. Так бывает, да.

Добавить ключ в память агента можно так:

$ ssh-add ~/.ssh/special_key.pem
Identity added: /home/gelin/.ssh/special_key.pem (/home/gelin/.ssh/special_key.pem)

Через SSH можно пробросить порты (port forwarding). То есть сделать так, чтобы, например, подключения на локальный TCP порт 8080 перенаправлялись на порт 80 некоторого процесса на сервере. При этом прямого подключения к 80 порту на сервере у вас нет. Но у вас есть SSH доступ на сервер.

$ ssh -L 8080:localhost:80 -N -f user@host.example.net

Это называется local forwarding. В данном случае слушается локальный порт 8080 клиента. На сервере происходит подключение к localhost:80 (можно перенаправить подключение и к другому хосту, доступному с сервера). SSH будет только пробрасывать порт, но не будет запускать shell на сервере: -N. SSH клиент уйдёт работать в фон: -f.

Local Forwarding

Можно и наоборот. Открыть на сервере порт для подключения, и все подключения на этот порт будут проброшены на другой хост и порт, доступные на клиенте. Это называется remote forwarding. Синтаксис такой же, но нужен ключик -R.

Remote Forwarding

SSH может пробрасывать и сессии X11. Если помните, протокол X, который всё ещё доминирует на наших юниксах как средство отрисовки гуя, обладает сетевой прозрачностью. Собственно, в Linux когда-то TCP/IP запихнули в спешке, лишь бы графические окошки рисовались. А значит, имея запущенный X server на SSH клиенте, вполне можно запустить гуёвое приложение где-нибудь на мощном сервере так, чтобы рисовало окошки оно на клиенте. И всё, что вам для этого нужно, это xlib на сервере и SSH доступ туда. (Запутались, кто клиент, а кто сервер? Правильно, в иксах это нормально.)

SSH может не просто пробрасывать порты на конкретные хосты, но и выступать в роли SOCKS сервера.

$ ssh -D 1080 -f -C -q -N user@host.example.net

Здесь у вас на локальном порту 1080 клиента появится SOCKS сервер, который может подключаться к любому хосту, доступному с сервера. Все подключения будут проброшены через SSH. Данные будут сжиматься: -C.

Конечно, полноценные VPN туннели работают быстрее и лучше. Но и SSH, как видите, вполне достаточно.

SOCKS Proxy

Ну и, конечно же, Ansible работает через SSH. Он загружает и выполняет свои скрипты на Python через SSH.

Делайте "ssh". Это очень мощная и полезная штука.