Co-Signer: развёртывание и привязка
Co-Signer — самообновляющийся узел, который вы запускаете в своём окружении. Он хранит вашу долю MPC-ключа и участвует в подписании исходящих транзакций вашего рабочего пространства: ключ целиком не существует ни на одной машине, поэтому ни платформа, ни кто-либо ещё не может подписать операцию без вашего узла.
Это руководство проведёт вас через весь путь: от запуска узла до проверки его статуса в консоли.
Как это работает
Co-Signer поставляется в виде лаунчера — небольшого процесса, который:
- запускает и контролирует основное приложение co-signer (перезапускает его при сбое);
- периодически проверяет наличие новой версии, верифицирует её подпись и обновляет приложение автоматически — ручного апдейта не требуется.
После привязки (pairing) узел открывает защищённое соединение с платформой, получает запросы на подпись по вашим транзакциям и выполняет MPC-протокол локально, своей долей ключа.
Предварительные требования
- Хост с Docker и исходящим доступом по HTTPS.
- Установленные
openssl,curl,jq. - Учётная запись подписанта в вашем рабочем пространстве (создаётся на Шаге 1).
- UUID рабочего пространства. Его можно увидеть в консоли.
Шаг 1. Ключ и учётная запись подписанта
Co-Signer аутентифицируется в платформе асимметричным ключом, который никогда не покидает ваше окружение. Сгенерируйте пару ключей Ed25519:
#!/usr/bin/env bash
# Генерация пары ключей Ed25519 для учётной записи подписанта co-signer.
# Использование: ./gen_keys.sh [--key private.pem] [--pub public.pem]
set -euo pipefail
KEY=private.pem
PUB=public.pem
while [[ $# -gt 0 ]]; do case "$1" in
--key) KEY=$2; shift 2 ;;
--pub) PUB=$2; shift 2 ;;
*) shift ;;
esac; done
# приватный ключ — храните в надёжном месте
openssl genpkey -algorithm ED25519 -out "$KEY"
# публичный ключ — его вы загрузите при создании учётной записи
openssl pkey -in "$KEY" -pubout -out "$PUB"
echo "Готово: $KEY (секрет), $PUB (для загрузки)."
Затем создайте учётную запись подписанта и загрузите public.pem — шаги
полностью совпадают с разделом Начало работы: создайте
пользователя с ролью SIGNER и приложите публичный ключ. В ответе вы
получите UUID учётной записи — он понадобится при привязке как
client_id.
Готовые скрипты
Команды ниже оформлены готовыми скриптами — их можно скачать по ссылке
под каждым блоком и запустить, не копируя текст: gen_keys.sh (этот
шаг) и pair_device_token.sh (шаг 3). Те же скрипты входят в исходную
поставку co-signer (каталог script/).
Шаг 2. Запуск лаунчера
Лаунчеру нужен ключ для шифрования локального хранилища узла. Сгенерируйте его один раз и сохраните — при его потере привязку придётся выполнять заново:
openssl rand -base64 16
Передайте этот ключ в переменной STORE_SECRET. Хранилище узла (привязка,
доля ключа) должно переживать перезапуск — выберите один из двух вариантов:
постоянный том или внешнее S3-совместимое хранилище.
Режим по умолчанию — переменная STORE_TYPE не нужна (её значение
file). Локальное хранилище вынесите на постоянный том:
services:
launcher:
image: cr.yandex/crpafl826pteg7mjj39j/launcher:latest
environment:
STORE_SECRET: "<ваш base64-ключ>"
volumes:
- "./storage:/.generated/storage:rw"
Включается переменной STORE_TYPE=s3. Состояние узла хранится в вашем
S3-совместимом бакете; том не нужен. Все значения шифруются ключом
STORE_SECRET до записи в бакет, поэтому доступ к бакету сам по себе
не раскрывает долю ключа. Не храните STORE_SECRET рядом с бакетом
(в том же секрет-хранилище с теми же правами доступа).
Данные узла лежат в бакете под префиксом STORE_S3_PREFIX. Префикс —
это идентичность узла: у каждого узла-подписанта должен быть свой
префикс; один и тот же префикс допустим только у резервного экземпляра
того же узла (см. предупреждение ниже).
services:
launcher:
image: cr.yandex/crpafl826pteg7mjj39j/launcher:latest
environment:
STORE_SECRET: "<ваш base64-ключ>"
STORE_TYPE: "s3"
STORE_S3_ENDPOINT: "https://s3.example.com"
STORE_S3_BUCKET: "co-signer-state"
STORE_S3_PREFIX: "co-signer-main"
AWS_ACCESS_KEY_ID: "<ключ доступа>"
AWS_SECRET_ACCESS_KEY: "<секретный ключ>"
| Переменная | Описание | По умолчанию |
|---|---|---|
STORE_TYPE |
file (локальный файл) или s3 |
file |
STORE_S3_ENDPOINT |
URL S3-совместимого эндпоинта | стандартное разрешение AWS |
STORE_S3_BUCKET |
Имя бакета | — (обязательна) |
STORE_S3_PREFIX |
Префикс ключей в бакете; свой у каждого узла | co-signer |
STORE_S3_REGION |
Регион | us-east-1 |
STORE_S3_PATH_STYLE |
Path-style адресация (true для MinIO-подобных) |
true |
STORE_S3_LEASE_TTL |
TTL lease-объекта в секундах, минимум 30 | 60 |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
Учётные данные доступа к бакету | — |
Один активный экземпляр на префикс
Доля ключа предполагает ровно один работающий экземпляр узла. Эксклюзивность обеспечивает lease-объект в бакете: второй экземпляр, запущенный с тем же префиксом, работает как резерв — дождётся освобождения lease и только потом продолжит запуск. При штатной остановке (SIGTERM) резерв подхватывает работу за секунды; после аварийного завершения — примерно через две минуты (истечение lease плюс защитная пауза).
Требования: S3-эндпоинт должен поддерживать условные записи
(If-Match / If-None-Match при PUT — узел проверяет это при старте
и не стартует, если поддержки нет), а часы хостов должны быть
синхронизированы по NTP (допуск — 15 секунд).
Миграция существующего узла с тома на S3
Перенос не требует новой привязки — данные копируются как есть:
- Остановите узел.
- Выполните разовый запуск приложения co-signer с флагом
--migrate-store, добавив переменныеSTORE_TYPE=s3иSTORE_S3_*: все ключи скопируются в бакет, после чего процесс завершится. В непустой префикс миграция не запускается. - Запустите узел с
STORE_TYPE=s3. Старый файл хранилища сохраните как резервную копию для отката.
Копирование выполняется целиком или не выполняется вовсе —
возобновить прерванную миграцию нельзя. Если она оборвалась
(сеть, истёкшие креды), процесс завершится с ошибкой, успев
записать часть ключей, и повторный запуск упрётся в отказ по
непустому префиксу. Сколько ключей успело скопироваться, видно
в логе — по полю keys_copied строки об ошибке миграции.
Как повторить:
- Не запускайте узел с
STORE_TYPE=s3в этом состоянии — в бакете лежит неполная копия. Исходный файл хранилища открывается только на чтение и не изменяется, поэтому актуальные данные всё ещё в нём. - Удалите незавершённую копию — объекты под
<префикс>/kv/. Соседний объект<префикс>/leaseне трогайте, если этот же префикс использует другой узел. - Повторите запуск с
--migrate-store.
docker compose up -d
docker compose logs -f launcher
После старта лаунчер скачает и запустит приложение co-signer. Пока
устройство не привязано, приложение ждёт токен привязки и слушает
локальный Unix-сокет /tmp/co-signer.sock.
Шаг 3. Привязка устройства
Привязка состоит из двух действий: вы запрашиваете у платформы токен привязки ключом своей учётной записи, а затем передаёте этот токен запущенному узлу.
3.1. Получите токен привязки
Подпишите запрос приватным ключом из шага 1 и запросите токен. Подписывается
тот же payload, что и при получении access-токена, —
здесь client_type равен user:
#!/usr/bin/env bash
# Запрос токена привязки co-signer.
# Использование: ./pair_device_token.sh --api-key KEY --workspace UUID [--key private.pem]
set -euo pipefail
API_BASE=https://auth.pert.paranoid.security/api/v2
KEY=private.pem
while [[ $# -gt 0 ]]; do case "$1" in
--api-key) API_KEY=$2; shift 2 ;;
--workspace) WORKSPACE_UUID=$2; shift 2 ;;
--key) KEY=$2; shift 2 ;;
--api-base) API_BASE=$2; shift 2 ;;
*) shift ;;
esac; done
: "${API_KEY:?укажите --api-key}" "${WORKSPACE_UUID:?укажите --workspace}"
# payload + подпись над сырыми байтами JSON.
DATA_FILE=$(mktemp); trap 'rm -f "$DATA_FILE"' EXIT
printf '{"client_id":"%s","client_type":"user","nonce":"%s","timestamp":%d}' \
"$API_KEY" "$(openssl rand -hex 16)" "$(date +%s)" > "$DATA_FILE"
DATA_B64=$(base64 < "$DATA_FILE" | tr -d '\n')
SIG_B64=$(openssl pkeyutl -sign -rawin -inkey "$KEY" -in "$DATA_FILE" | base64 | tr -d '\n')
# POST к API; при HTTP-ошибке печатает тело ответа и прерывает скрипт
api() {
local out code body
out=$(curl -sS -w '\n%{http_code}' "$@")
code=${out##*$'\n'}; body=${out%$'\n'*}
[[ $code == 2* ]] || { echo "ошибка API ($code): $body" >&2; exit 1; }
printf '%s' "$body"
}
# 1. access-токен
ACCESS_TOKEN=$(api -X POST "$API_BASE/auth/access-token" \
-H 'Content-Type: application/json' \
-d "{\"data\":\"$DATA_B64\",\"signature\":\"$SIG_B64\"}" | jq -r .access_token)
# 2. инициируем привязку и получаем токен
PAIRING_TOKEN=$(api -X POST "$API_BASE/auth/devices/pairing/initiate" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"tenant_uuid\":\"$WORKSPACE_UUID\"}" | jq -r .pairing_token)
echo "$PAIRING_TOKEN"
Запустите скрипт, передав параметры через флаги; путь к приватному ключу
задаётся --key (по умолчанию private.pem). Токен печатается в stdout:
PAIRING_TOKEN=$(./pair_device_token.sh \
--api-key "<API KEY>" \
--workspace "<UUID рабочего пространства>" \
--key private.pem)
3.2. Передайте токен узлу
Отправьте полученный токен на локальный сокет co-signer — узел завершит привязку, сохранит привязку устройства и откроет соединение с платформой:
docker compose exec launcher \
curl --unix-socket /tmp/co-signer.sock http://localhost/pair-device \
-H 'Content-Type: application/json' \
-d "{\"pairing_token\":\"$PAIRING_TOKEN\"}"
Сокет внутри контейнера
При запуске в Docker сокет /tmp/co-signer.sock находится внутри
контейнера лаунчера. Выполняйте команду через docker compose exec
(как выше) либо пробросьте сокет на хост отдельным томом.
Успешный ответ означает, что устройство привязано. С этого момента узел держит соединение с платформой и принимает запросы на подпись по транзакциям вашего рабочего пространства.
Чтобы узел начал подписывать
Привязка только подключает узел к платформе — сама по себе она не
отправляет ему транзакции на подпись. Чтобы операции уходили на этот
co-signer, в рабочем пространстве должно быть настроено
Transfer-правило, которое назначает его подписантом (блок Signer —
по UUID учётной записи подписанта из Шага 1). Без подходящего
правила запросы на подпись на узел не приходят. Подробнее —
Transfer policy.
Шаг 4. Проверка статуса в консоли
Откройте в консоли раздел Косайнеры. После первого отчёта узла в таблице появится строка с его состоянием:
| Колонка | Что показывает |
|---|---|
| Имя | Имя узла (или ID устройства, если имя не задано) |
| Статус | Онлайн / С ошибками / Офлайн |
| Версия | Версия приложения (и лаунчера) |
| Последний контакт | Время последнего отчёта узла |
| Аптайм | Время непрерывной работы |
| Очередь | Число запросов на подпись, ожидающих обработки |
Список обновляется автоматически каждые ~20 секунд. Разверните строку, чтобы увидеть детали: ID устройства, учётную запись, ОС, IP-адрес, дату привязки и последнюю ошибку, если она была.
Узел ещё не появился?
Пока узел не отчитался ни разу, раздел показывает «Косайнеров пока нет». Если строка не появляется после успешной привязки — проверьте, что у хоста есть исходящий HTTPS-доступ, и посмотрите логи лаунчера.
Обновления
Лаунчер сам следит за выходом новых версий, проверяет их подпись и обновляет приложение co-signer. Со стороны оператора никаких действий не требуется — после обновления узел перезапускается автоматически, а в консоли обновляется поле Версия.
Отвязка
Чтобы отключить узел от рабочего пространства, в разделе Косайнеры откройте меню строки и выберите Отвязать. После этого узел теряет доступ к этому рабочему пространству и перестаёт получать запросы на подпись. Отвязка действует только на текущее рабочее пространство — в других узел продолжает работать.