Сценарий знаком многим администраторам. На рабочей машине стоит десяток клиентов для удалённых подключений - PuTTY для SSH, Remote Desktop Connection для Windows-серверов, какой-нибудь TightVNC для старых машин. Каждый со своими настройками, профилями, сохранёнными паролями. При смене ноутбука всё это богатство переносится с приключениями. А если нужно подключиться с чужого компьютера или планшета, то начинается отдельная история про установку нужных клиентов.

Apache Guacamole решает эту головную боль одним приёмом. Это бесплатное приложение с открытым исходным кодом для безклиентского удалённого доступа к рабочим столам и серверам через обычный веб-браузер. Платформа поддерживает протоколы VNC, RDP и SSH, а отображение делается через HTML5. На клиентской стороне не нужно ставить вообще ничего - открыл браузер, ввёл адрес сервера, выбрал нужное подключение и работаешь. Работает в большинстве дистрибутивов Linux, а клиент запускается в любом современном браузере. Ниже разобрана пошаговая установка серверной части Guacamole и клиента через Docker на Ubuntu 22.04.

Подготовка чистого сервера и предварительные требования для развёртывания связки контейнеров

Понадобится сервер с установленной Ubuntu 22.04 LTS. Минимальные требования к ресурсам у Guacamole скромные, но с учётом всей связки из трёх контейнеров (база данных, сервер и клиент) стоит закладывать как минимум 2 гигабайта оперативной памяти и пару процессорных ядер. На сервере должен быть настроен root-пароль, поскольку часть команд требует привилегий суперпользователя. Все команды в туториале выполняются от имени root - если работа идёт под обычным пользователем, перед каждой командой подставляйте sudo.

Архитектурно Guacamole в контейнеризованном виде разбит на три отдельных компонента. Первый - база данных MySQL, в которой хранятся учётки пользователей, описания соединений и история подключений. Второй - сам сервер Guacamole (guacd), который реализует протоколы VNC, RDP и SSH и преобразует их в поток данных для веб-клиента. Третий - веб-клиент (guacamole), который рендерит интерфейс в браузере и общается с guacd через свой внутренний протокол. Такое разделение даёт гибкость - например, можно держать базу на отдельной выделенной машине или масштабировать клиентскую часть горизонтально.

Установка движка Docker Engine из официального репозитория для последующего запуска компонентов Guacamole

Перед установкой контейнеров нужен сам Docker. В стандартных репозиториях Ubuntu лежит пакет docker.io, но он отстаёт от актуальных релизов на несколько месяцев. Для production-сценариев правильнее тянуть свежую сборку прямо из репозитория Docker.

Сначала ставятся вспомогательные пакеты, без которых не получится корректно подключить внешний репозиторий по HTTPS.

apt install ca-certificates curl gnupg lsb-release -y

Сюда входят корневые сертификаты, утилита curl для скачивания GPG-ключа, набор gnupg для работы с подписями и lsb-release для определения кодового имени релиза Ubuntu. Флаг -y подтверждает все запросы автоматически.

Добавляем GPG-ключ Docker и сам репозиторий в список источников APT.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor > /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list

Флаги curl стоит разобрать отдельно. Опция -f возвращает ошибку при HTTP-неудачах вместо тихого сохранения страницы с ошибкой. Параметр -s отключает прогресс-бар, -S всё-таки оставляет вывод сообщений об ошибках, а -L разрешает следовать редиректам. Утилита gpg --dearmor конвертирует текстовый ASCII-armored ключ в бинарный формат для APT. Команда dpkg --print-architecture возвращает архитектуру системы (обычно amd64), а lsb_release -cs выдаёт кодовое имя релиза - для 22.04 это jammy.

Обновляем кэш и устанавливаем сам движок.

apt update -y
apt install docker-ce docker-ce-cli containerd.io -y

Здесь ставятся три пакета. Первый, docker-ce, это собственно демон-движок, который рулит всеми контейнерами. Второй, docker-ce-cli, добавляет утилиту командной строки docker. Третий, containerd.io, представляет собой низкоуровневый рантайм, на котором фактически крутятся контейнеры.

Проверка установленной версии.

docker --version

Вывод подтвердит работоспособность.

Docker version 20.10.18, build b40c2f6

Конкретный номер сборки зависит от момента установки.

Установка Docker Compose для удобного управления связкой из нескольких контейнеров

Docker Compose позволяет описывать многоконтейнерные приложения декларативно через YAML-файл и поднимать всю связку одной командой. В рамках текущего туториала контейнеры запускаются вручную через docker run, но Compose всё равно ставится для последующих более сложных сценариев.

curl -sL "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

Команда curl с параметрами -sL скачивает бинарник Compose под текущую архитектуру. Подстановки $(uname -s) и $(uname -m) автоматически определяют операционную систему (Linux) и архитектуру процессора (x86_64 или aarch64), что делает команду универсальной. Утилита chmod +x делает скачанный файл исполняемым.

Проверка установленной версии.

docker-compose --version
docker-compose version 1.29.2, build 5becea4c

Загрузка образов Guacamole-сервера и клиента из Docker Hub перед запуском контейнеров

Перед запуском контейнеров полезно отдельно загрузить нужные образы. Это разделяет фазу скачивания (которая может занять время на медленном канале) и фазу запуска, плюс позволяет проверить корректность образов до старта.

Загрузка серверной части Guacamole.

docker pull guacamole/guacd

Вывод покажет процесс по слоям.

Using default tag: latest
latest: Pulling from guacamole/guacd
4b7b4a8876e2: Pull complete 
4e542e9cf89d: Pull complete 
9741340fbbb2: Pull complete 
96fa725029d6: Pull complete 
0f0a6df13f2a: Pull complete 
a4fa6e99a790: Pull complete 
07365dfaa371: Pull complete 
Digest: sha256:efdc09beba21e2c5d7c8c77e8c6b95ffd173d75645c9c41b6024f5b835d2a034
Status: Downloaded newer image for guacamole/guacd:latest
docker.io/guacamole/guacd:latest

Каждая строка с Pull complete означает успешно загруженный слой. Контейнерные образы устроены как набор слоёв, что экономит трафик и место при наличии общих базовых слоёв с другими образами. Длинная строка с Digest - криптографическая подпись для проверки целостности.

Загрузка клиентской части.

docker pull guacamole/guacamole

Процесс аналогичный.

Using default tag: latest
latest: Pulling from guacamole/guacamole
2b55860d4c66: Pull complete 
2ca45fc4c4ca: Pull complete 
0cd32add6672: Pull complete 
ac52cbbb8ca2: Pull complete 
7f7dc3a9f4cc: Pull complete 
5d7996a24402: Pull complete 
4819d3e4118d: Pull complete 
055afbac1f72: Pull complete 
23ee772344b7: Pull complete 
e44569de6126: Pull complete 
f7d7e8014b18: Pull complete 
Digest: sha256:8a8db8cf9f5359aa20547382213a42a720ea1c5fe86460ded727061e1995d9f2
Status: Downloaded newer image for guacamole/guacamole:latest
docker.io/guacamole/guacamole:latest

Клиентский образ толще серверного - в нём лежит веб-приложение со всеми статиками, JavaScript-кодом и зависимостями.

Проверка списка скачанных образов.

docker images

Таблица покажет оба образа с их идентификаторами и размерами.

REPOSITORY            TAG       IMAGE ID       CREATED        SIZE
guacamole/guacd       latest    4086ac9e35a7   9 hours ago    271MB
guacamole/guacamole   latest    959856a45436   10 hours ago   432MB

Серверный guacd весит 271 мегабайт, клиент guacamole около 432 мегабайт - вполне типичные значения для контейнеров с полноценной функциональностью.

Запуск контейнера с базой данных MySQL и наполнение её схемой для нужд Guacamole

Guacamole требует базы данных для хранения учёток пользователей и описаний соединений. Контейнер MySQL стартует следующей командой.

docker run --name guacamoledb -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=guacdb -d mysql/mysql-server

Разберём параметры. Флаг --name задаёт человекочитаемое имя контейнера для последующих обращений. Опции -e задают переменные окружения внутри контейнера. Переменная MYSQL_ROOT_PASSWORD устанавливает пароль root-пользователя MySQL - в production это должен быть длинный криптостойкий пароль, а не строка password из примера. Переменная MYSQL_DATABASE автоматически создаёт базу с указанным именем при старте контейнера. Флаг -d переводит контейнер в фоновый режим. Последний аргумент - имя образа из официального репозитория MySQL.

Вывод покажет загрузку образа и идентификатор созданного контейнера.

latest: Pulling from mysql/mysql-server
cdd8b07c6082: Pull complete 
c2f1720beca1: Pull complete 
39f143a8d6de: Pull complete 
118a8285b641: Pull complete 
b45cbcaf75c7: Pull complete 
d4574372e600: Pull complete 
1f565a3cbc52: Pull complete 
Digest: sha256:e30a0320f2e3c7b7ee18ab903986ada6eb1ce8e5ef29941b36ec331fae5f10b2
Status: Downloaded newer image for mysql/mysql-server:latest
c7a9309eac20a7d0bb6f0a16460bf2b678aae741c201efae8974ea64a3736595

Создаём каталог для хранения скрипта инициализации базы.

mkdir -p /opt/guacamole/mysql

Флаг -p создаст всю цепочку каталогов разом, если промежуточных папок не существует.

Образ guacamole/guacamole содержит вспомогательный скрипт, который умеет генерировать SQL для создания схемы базы. Запускаем его через одноразовый контейнер и сохраняем результат в файл.

docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > /opt/guacamole/mysql/01-initdb.sql

Параметр --rm заставит Docker удалить контейнер сразу после завершения работы команды - идеальный вариант для одноразовых утилитарных запусков. Флаг --mysql указывает скрипту генерировать SQL именно под MySQL (поддерживается также PostgreSQL и SQL Server). Перенаправление через > сохраняет вывод в локальный файл.

Копируем сгенерированный SQL внутрь работающего контейнера MySQL.

docker cp /opt/guacamole/mysql/01-initdb.sql guacamoledb:/docker-entrypoint-initdb.d

Утилита docker cp работает в обе стороны - можно копировать как с хоста в контейнер, так и обратно. Каталог /docker-entrypoint-initdb.d внутри образа MySQL имеет особое значение - все SQL-файлы оттуда автоматически выполняются при первой инициализации базы.

Подключаемся к контейнеру MySQL через интерактивный bash.

docker exec -it guacamoledb bash

Флаг -i делает stdin интерактивным, флаг -t выделяет псевдо-терминал. Без этой связки команды работают, но интерактивная оболочка получается кривой.

Приглашение терминала меняется на bash контейнера.

bash-4.4# 

Проверяем, что SQL-файл действительно лёг в нужный каталог.

cd /docker-entrypoint-initdb.d/
ls
01-initdb.sql

Подключаемся к shell самого MySQL.

mysql -u root -p

При запросе пароля вводим тот, что был задан через переменную окружения MYSQL_ROOT_PASSWORD.

Переключаемся на базу guacdb и выполняем скрипт инициализации.

use guacdb;
source 01-initdb.sql;

Команда source в MySQL-клиенте читает указанный файл и построчно выполняет содержащиеся в нём команды. Это удобный способ применить большой SQL-скрипт без копирования его в командную строку.

Проверяем созданные таблицы.

show tables;

В выводе появится длинный список таблиц Guacamole.

+---------------------------------------+
| Tables_in_guacdb                      |
+---------------------------------------+
| guacamole_connection                  |
| guacamole_connection_attribute        |
| guacamole_connection_group            |
| guacamole_connection_group_attribute  |
| guacamole_connection_group_permission |
| guacamole_connection_history          |
| guacamole_connection_parameter        |
| guacamole_connection_permission       |
| guacamole_entity                      |
| guacamole_sharing_profile             |
| guacamole_sharing_profile_attribute   |
| guacamole_sharing_profile_parameter   |
| guacamole_sharing_profile_permission  |
| guacamole_system_permission           |
| guacamole_user                        |
| guacamole_user_attribute              |
| guacamole_user_group                  |
| guacamole_user_group_attribute        |
| guacamole_user_group_member           |
| guacamole_user_group_permission       |
| guacamole_user_history                |
| guacamole_user_password_history       |
| guacamole_user_permission             |
+---------------------------------------+

Каждая таблица отвечает за свой кусок функциональности. Например, guacamole_user хранит учётки пользователей, guacamole_connection описывает настроенные подключения, guacamole_connection_history фиксирует историю сеансов работы. Такая нормализованная структура позволяет гибко управлять правами доступа на уровне отдельных соединений и групп пользователей.

Создаём отдельную учётку MySQL для работы Guacamole с базой - использовать root для приложения было бы грубейшим нарушением принципа минимальных привилегий.

create user guacadmin@'%' identified by 'password';
grant SELECT,UPDATE,INSERT,DELETE on guacdb.* to guacadmin@'%';

Знак процента означает, что подключаться этой учёткой можно с любого хоста (в данном случае - из соседнего Docker-контейнера). Выдаём только четыре базовых права - SELECT для чтения, UPDATE для изменений, INSERT для добавления записей, DELETE для удаления. Привилегий вроде DROP или ALTER, способных снести таблицы целиком, у учётки приложения быть не должно.

Сбрасываем кэш привилегий и выходим из MySQL.

flush privileges;
exit;

Выходим из bash-оболочки контейнера.

exit

Проверяем, что контейнер базы работает.

docker ps

В выводе должна появиться строка с guacamoledb и статусом healthy.

CONTAINER ID   IMAGE                COMMAND                  CREATED         STATUS                   PORTS                       NAMES
c7a9309eac20   mysql/mysql-server   "/entrypoint.sh mysq…"   2 minutes ago   Up 2 minutes (healthy)   3306/tcp, 33060-33061/tcp   guacamoledb

Метка healthy в статусе означает, что встроенная проверка работоспособности образа отрабатывает успешно - база отвечает на тестовые запросы.

При желании можно посмотреть логи запуска MySQL.

docker logs guacamoledb

В выводе будут стандартные сообщения о запуске сервера.

[Entrypoint] Starting MySQL 8.0.30-1.2.9-server
2022-10-02T11:31:52.589985Z 0 [Warning] [MY-011068] [Server] The syntax '--skip-host-cache' is deprecated and will be removed in a future release. Please use SET GLOBAL host_cache_size=0 instead.
2022-10-02T11:31:52.592352Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.30) starting as process 1
2022-10-02T11:31:52.602604Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2022-10-02T11:31:52.875859Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2022-10-02T11:31:53.131572Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2022-10-02T11:31:53.131639Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
2022-10-02T11:31:53.163561Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
2022-10-02T11:31:53.163648Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.30'  socket: '/var/lib/mysql/mysql.sock'  port: 3306  MySQL Community Server - GPL.

Строка ready for connections внизу подтверждает, что база полностью готова принимать запросы.

Запуск серверной части Apache Guacamole в виде отдельного контейнера

Серверная часть guacd работает как фоновый демон, принимающий запросы от веб-клиента и устанавливающий реальные удалённые подключения по VNC, RDP или SSH.

docker run --name guacamole-server -d guacamole/guacd

Запуск минималистичный - только имя и фоновый режим. Никаких переменных окружения или проброса портов наружу не нужно, поскольку guacd общается только с веб-клиентом через внутреннюю Docker-сеть.

Проверка логов запуска.

docker logs --tail 10 guacamole-server

Параметр --tail 10 показывает только последние 10 строк - удобно для быстрого взгляда без листания тонны вывода.

guacd[7]: INFO:	Guacamole proxy daemon (guacd) version 1.4.0 started
guacd[7]: INFO:	Listening on host 0.0.0.0, port 4822

Строка о прослушивании порта 4822 подтверждает успешный старт. Этот порт открыт только внутри Docker-сети и не виден снаружи хоста, что хорошо для безопасности.

Проверка списка работающих контейнеров.

docker ps
CONTAINER ID   IMAGE                COMMAND                  CREATED          STATUS                             PORTS                       NAMES
51b2efdab0db   guacamole/guacd      "/bin/sh -c '/usr/lo…"   26 seconds ago   Up 25 seconds (health: starting)   4822/tcp                    guacamole-server
c7a9309eac20   mysql/mysql-server   "/entrypoint.sh mysq…"   3 minutes ago    Up 3 minutes (healthy)             3306/tcp, 33060-33061/tcp   guacamoledb

Два контейнера крутятся, статус health starting у guacd говорит о том, что проверка работоспособности ещё проводит начальные тесты. Через минуту-другую он переключится на healthy.

Запуск веб-клиента Guacamole со связками к базе и серверу через механизм Docker links

Финальный кусок мозаики - веб-клиент, который связывает базу данных и сервер guacd в единое целое и выставляет наружу веб-интерфейс.

docker run --name guacamole-client --link guacamole-server:guacd --link guacamoledb:mysql -e MYSQL_DATABASE=guacdb -e MYSQL_USER=guacadmin -e MYSQL_PASSWORD=password -d -p 80:8080 guacamole/guacamole

Команда длинная, поэтому стоит разобрать её по частям. Параметр --name guacamole-client задаёт имя контейнера. Флаги --link создают традиционные Docker-связки между контейнерами - --link guacamole-server:guacd делает контейнер с сервером guacd доступным изнутри клиентского контейнера под алиасом guacd, аналогично --link guacamoledb:mysql даёт доступ к базе под алиасом mysql.

Стоит заметить, что механизм --link считается устаревшим в современном Docker и вместо него рекомендуется использовать пользовательские сети через docker network create. Но для простых сценариев links продолжают работать и удобны своей наглядностью.

Параметры -e задают переменные окружения с настройками подключения к базе - имя базы guacdb, имя пользователя guacadmin и пароль. Эти значения соответствуют тому, что было настроено в MySQL-контейнере.

Флаг -d запускает контейнер в фоне. Параметр -p 80:8080 пробрасывает порт - снаружи на хосте слушается 80-й порт, внутри контейнера приложение работает на 8080.

Проверка списка контейнеров.

docker ps

Теперь все три контейнера видны в выводе.

CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS                                 PORTS                                   NAMES
d4034a72bb69   guacamole/guacamole   "/opt/guacamole/bin/…"   58 seconds ago       Up 57 seconds                          0.0.0.0:80->8080/tcp, :::80->8080/tcp   guacamole-client
51b2efdab0db   guacamole/guacd       "/bin/sh -c '/usr/lo…"   About a minute ago   Up About a minute (health: starting)   4822/tcp                                guacamole-server
c7a9309eac20   mysql/mysql-server    "/entrypoint.sh mysq…"   5 minutes ago        Up 5 minutes (healthy)                 3306/tcp, 33060-33061/tcp               guacamoledb

В строке клиента видна публикация порта 0.0.0.0:80->8080/tcp - это означает, что запросы из внешней сети на 80-й порт перенаправляются на 8080-й внутри контейнера.

Проверяем прослушивание 80-го порта на уровне хоста.

ss -altnp | grep :80

Опции утилиты ss перечислены через объединённый флаг. Параметр -a выводит все сокеты, -l ограничивает выводом только слушающие сокеты, -t показывает только TCP, -n выводит порты числами без разрешения в имена, -p добавляет информацию о процессе. Конвейер через grep отфильтровывает строки с подстрокой :80.

LISTEN 0      4096         0.0.0.0:80        0.0.0.0:*    users:(("docker-proxy",pid=4006,fd=4))   
LISTEN 0      4096            [::]:80           [::]:*    users:(("docker-proxy",pid=4013,fd=4))   

Процесс docker-proxy - это вспомогательный механизм Docker для проброса портов между хостовой сетью и внутренней сетью контейнеров. Видим прослушивание и на IPv4, и на IPv6.

Доступ к веб-интерфейсу Guacamole и первичная настройка через браузер

Наконец, самый приятный момент - открытие веб-интерфейса. В любом современном браузере вводим адрес http://адрес-сервера/guacamole. Если сервер находится в локальной сети с IP вроде 192.168.1.50, адрес будет http://192.168.1.50/guacamole. Для облачного сервера используется его публичный IP или привязанное доменное имя.

Откроется экран входа с логотипом Guacamole. Учётные данные по умолчанию хорошо известны и не блещут оригинальностью.

User: guacadmin
Password: guacadmin

После клика по кнопке Login открывается основная панель управления Guacamole. Первое, что обязательно нужно сделать после входа - сменить пароль администратора. Дефолтные учётки руcского сорта хорошо известны автоматическим сканерам, и оставлять их означает приглашать незваных гостей. Смена пароля делается через настройки профиля в правом верхнем углу интерфейса.

После смены пароля можно приступать к настройке самих подключений. Guacamole поддерживает создание соединений по SSH для серверов Linux, RDP для машин Windows и VNC для всего остального. У каждого протокола свой набор параметров - адрес сервера, порт, учётные данные, разрешение экрана и так далее. Соединения можно объединять в группы для удобства навигации, раздавать права доступа отдельным пользователям и сохранять историю сеансов.

Практические сценарии использования Guacamole и типичные подводные камни при эксплуатации

Несколько моментов из практики, которые стоит держать в голове при работе с развёрнутой системой.

  1. Дефолтный пароль guacadmin нужно сменить сразу после первого входа. Это единственный способ закрыть лазейку для автоматических атак из интернета.
  2. Связки --link при подходе через docker run считаются устаревшими. Для production-сценариев лучше использовать Docker Compose с описанием связки через networks - это даёт больше гибкости и контроля.
  3. Хранение пароля password для базы данных в туториальных целях нормально, но в реальной эксплуатации все пароли должны быть длинными случайными строками. Утечка такого пароля даёт атакующему полный доступ ко всем учётным данным удалённых серверов.
  4. Доступ к Guacamole стоит закрыть HTTPS-сертификатом через обратный прокси Nginx или Traefik перед контейнером. Передавать пароли и сессии в открытом виде по 80-му порту небезопасно.
  5. Регулярные обновления образов через docker pull guacamole/guacamole и последующий перезапуск контейнеров закрывают накопленные уязвимости. На большой инфраструктуре стоит автоматизировать это через инструменты вроде Watchtower.
  6. Бэкап базы данных MySQL делается через docker exec guacamoledb mysqldump -uroot -ppassword guacdb > backup.sql. Регулярные бэкапы - страховка от потери всех настроенных подключений.

Освоение такого инструмента превращает повседневный труд системного администратора. Вместо разрозненной коллекции клиентов и сохранённых паролей появляется единая точка доступа ко всему парку машин через любой браузер. Можно подключиться с планшета в кафе, с чужого ноутбука в командировке, с домашнего компьютера выходного дня - функциональность везде одна и та же. Для команд это значит общий каталог подключений с разграничением прав, прозрачную историю сеансов и единую политику паролей. Apache Guacamole не пытается заменить полноценные RDP или VNC клиенты для тяжёлой работы с графикой, но в большинстве сценариев администрирования его веб-интерфейс справляется превосходно. И самое приятное - вся эта мощь умещается в три Docker-контейнера, которые ставятся на любой сервер за полчаса.