Блог

gorst: простой способ управлять своими сервисами

С чего всё началось

Первую версию я написал в 2024 году. Это был небольшой REST API на Go. Сервис принимал запрос с API-ключом и перезапускал один из юнитов systemd.

bash
curl -X POST http://127.0.0.1:4035/api/v1.0/service \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"service": "dnl-hns", "method": "restart"}'

Список сервисов и действий, разрешённых для каждого из них, лежал в одном config.json. Из того же списка собирался белый список для sudo, так что через API нельзя было запустить ничего лишнего. За сервисом стоял простой rate limiter.

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

Что gorst умеет сейчас

Сейчас gorst состоит из двух частей.

  • worker работает на каждом сервере и управляет его юнитами systemd;
  • контрольный узел (gorst-controlnode) хранит пользователей, политики и журнал действий, и все команды идут через него.

Контрольный узел решает, кто и что может делать и на каком сервере. Допустим, у вас есть сервер с nginx, и нужно, чтобы кто-то ещё мог его перезапускать, но без SSH и root. Вы создаёте политику, которая разрешает действие restart для nginx на этом сервере, и выдаёте её пользователю. Больше ему ничего не доступно. Перезапустить nginx он может из веб-панели, где у каждого сервиса видны только разрешённые ему действия, или через API по токену. Токен можно встроить в свои инструменты, например в бота для Telegram или Discord, который перезапускает сервис по команде из чата.

Панель gorst, страница worker dnl с его сертификатом, выводом status dnl-hns и списком сервисов с кнопками действий

Как это устроено

Архитектура gorst. Worker на каждой машине сам подключается к контрольному узлу одним gRPC-потоком с mutual TLS, браузеры и боты обращаются к контрольному узлу по HTTPS, пользователи, политики и журнал хранятся в PostgreSQL

Worker сам подключается к контрольному узлу, а узел к нему не подключается никогда. При старте worker открывает один исходящий gRPC-поток и ждёт по нему команды. Поэтому на серверах не нужно открывать порты, и машина за NAT работает так же, как VPS с публичным IP.

Соединение защищено mutual TLS. При первом запуске контрольный узел создаёт свой центр сертификации и дальше выдаёт сертификат каждому worker. Продлевают сертификаты worker сами. Если сервер больше не нужен или его ключ утёк, сертификат отзывается, и эта машина больше не сможет подключиться.

Установка

1. Сборка

Склонируйте репозиторий на сервер и соберите нужный ему бинарник. Контрольный узел и worker собираются отдельно, понадобится Go 1.27.1 или новее.

bash
git clone https://github.com/happydez/gorst.git
cd gorst

# на сервере с контрольным узлом
CGO_ENABLED=0 go build -o bin/gorst-controlnode ./cmd/gorst-controlnode

# на каждом сервере с worker
CGO_ENABLED=0 go build -o bin/gorst ./cmd/gorst

Установочные скрипты в deploy/ сами берут бинарник из bin/, поэтому вручную ничего копировать не нужно.

2. Контрольный узел

Перед установкой сделайте свой конфиг рядом со скриптами. Если есть deploy/controlnode.yaml, установщик возьмёт его, а если нет, возьмёт конфиг .example.

bash
cp deploy/controlnode.example.yaml deploy/controlnode.yaml
yaml
agent:
  addr: "0.0.0.0:4040"       # сюда подключаются worker
  hosts:                     # имена и адреса, по которым worker будут подключаться
    - cn.example.com

admin:
  addr: "127.0.0.1:4050"     # веб-панель и API

database:
  dsn: "postgres://gorst:...@127.0.0.1:5432/gorst?sslmode=disable"

Имена из agent.hosts попадают в сертификат контрольного узла, и если worker подключится по другому имени, он не станет ему доверять. Панель лучше оставить на 127.0.0.1 за reverse proxy с HTTPS или дать ей отдельный сертификат в admin.tls. Без database.dsn контрольный узел тоже запустится, но при каждом перезапуске будет терять пользователей и журнал.

Теперь установите его и создайте администратора.

bash
sudo bash deploy/install-controlnode.sh
sudo gorst-controlnode user add --admin --password '...' admin

Установщик создаёт системного пользователя gorst-cn, кладёт конфиг в /etc/gorst-controlnode/config.yaml, хранит центр сертификации в /var/lib/gorst-controlnode и запускает сервис. Логи можно посмотреть командой journalctl -u gorst-controlnode -f.

3. Worker на каждом сервере

Worker ставится так же. В его конфиге перечислены юниты, которыми gorst вообще разрешено управлять. Из этого списка установщик собирает белый список для sudo и проверяет его через visudo.

bash
cp deploy/config.example.yaml deploy/config.yaml
yaml
services:
  nginx:
    actions: [status, reload, restart]
bash
sudo bash deploy/install.sh

Конфиг окажется в /etc/gorst/config.yaml, а сервис будет работать от непривилегированного пользователя gorst. Если потом поменяете блок services, просто запустите установщик ещё раз. Конфиг он не тронет, а белый список для sudo пересоберёт.

4. Подключение к контрольному узлу

join-token обращается к API запущенного контрольного узла, а если у узла есть база, это API принимает только токен администратора. Поэтому сначала выпустите токен для учётной записи admin. Он показывается один раз, так что сохраните его.

bash
sudo gorst-controlnode token create --name cli admin

С этим токеном запросите одноразовый токен для подключения. Вместе с ним выводится хеш сертификата CA.

bash
gorst-controlnode join-token --admin <admin.addr> --token <токен администратора>

В --admin укажите адрес панели из admin.addr в конфиге контрольного узла. Если панель работает по HTTPS, добавьте --tls.

На worker обменяйте этот токен на сертификат.

bash
sudo gorst join --controlnode cn.example.com:4040 --token ... --ca-hash sha256:...

gorst join создаёт ключ, получает подписанный сертификат и выводит блок controlnode. Команды от контрольного узла выполняются под локальной политикой, имя которой указано в этом блоке, поэтому сначала добавьте её в список policies в /etc/gorst/config.yaml. Токен ей не нужен, контрольный узел подтверждает себя сертификатом.

yaml
policies:
  - name: controlnode
    services:
      nginx: ["*"]

Затем вставьте выведенный блок в конец файла и перезапустите worker.

yaml
controlnode:
  addr: "cn.example.com:4040"
  ca_cert: /etc/gorst/ca.pem
  cert: /etc/gorst/worker.pem
  key: /etc/gorst/worker-key.pem
  policy: controlnode
bash
sudo systemctl restart gorst

После перезапуска сервер появится в панели на странице Workers под своим hostname. Этот же список можно получить из командной строки.

bash
gorst-controlnode workers --admin <admin.addr> --token <токен администратора>

5. Права и токены

Пока политика не выдаст пользователю доступ, он ничего не видит. В праве указываются сервер, сервис и действие, и вместо любого из них можно поставить *. Здесь пользователь happydez получает право перезапускать nginx на server-1, и больше ничего.

bash
sudo gorst-controlnode policy add --description 'перезапуск nginx' nginx-restart
sudo gorst-controlnode policy allow nginx-restart server-1 nginx restart

sudo gorst-controlnode user add --password '...' happydez
sudo gorst-controlnode user grant happydez nginx-restart

Бот в gorst это обычный пользователь без пароля, но с токеном.

bash
sudo gorst-controlnode user add deploy-bot
sudo gorst-controlnode user grant deploy-bot nginx-restart
sudo gorst-controlnode token create --name telegram deploy-bot   # токен показывается один раз

Токен действует от имени своего владельца и ограничен теми же политиками, так что бот сможет перезапустить nginx на server-1, и ничего больше. Для бота это один HTTP-запрос.

go
func restartNginx(ctx context.Context) error {
	url := "https://cn.example.com/api/v1/workers/server-1/nginx/restart"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GORST_TOKEN"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("gorst: %s", resp.Status)
	}

	return nil
}

Полная документация, описание REST и gRPC API и все параметры конфигов есть в репозитории github.com/happydez/gorst.

Написать мне

Вопрос, предложение или просто привет