Перейти к содержанию

Co-Signer: развёртывание и привязка

Co-Signer — самообновляющийся узел, который вы запускаете в своём окружении. Он хранит вашу долю MPC-ключа и участвует в подписании исходящих транзакций вашего рабочего пространства: ключ целиком не существует ни на одной машине, поэтому ни платформа, ни кто-либо ещё не может подписать операцию без вашего узла.

Это руководство проведёт вас через весь путь: от запуска узла до проверки его статуса в консоли.

Как это работает

Co-Signer поставляется в виде лаунчера — небольшого процесса, который:

  • запускает и контролирует основное приложение co-signer (перезапускает его при сбое);
  • периодически проверяет наличие новой версии, верифицирует её подпись и обновляет приложение автоматически — ручного апдейта не требуется.

После привязки (pairing) узел открывает защищённое соединение с платформой, получает запросы на подпись по вашим транзакциям и выполняет MPC-протокол локально, своей долей ключа.

........Co-SignerPert....... PertОператорОператорCo-Signer(ваш узел)Co-Signer(ваш узел)PertPertКонсоль PertКонсоль Pert1запуск лаунчера2генерация ключей устройства3запрос токена привязки(подпись ключом учётной записи)4pairing token5pair-device(token)(локальный сокет)6завершение привязки(ключ устройства)7ok (device id)8постоянное соединение,приём запросов на подпись9статус узла = «Онлайн»
........Co-SignerPert....... PertОператорОператорCo-Signer(ваш узел)Co-Signer(ваш узел)PertPertКонсоль PertКонсоль Pert1запуск лаунчера2генерация ключей устройства3запрос токена привязки(подпись ключом учётной записи)4pairing token5pair-device(token)(локальный сокет)6завершение привязки(ключ устройства)7ok (device id)8постоянное соединение,приём запросов на подпись9статус узла = «Онлайн»

Предварительные требования

  • Хост с Docker и исходящим доступом по HTTPS.
  • Установленные openssl, curl, jq.
  • Учётная запись подписанта в вашем рабочем пространстве (создаётся на Шаге 1).
  • UUID рабочего пространства. Его можно увидеть в консоли.

Шаг 1. Ключ и учётная запись подписанта

Co-Signer аутентифицируется в платформе асимметричным ключом, который никогда не покидает ваше окружение. Сгенерируйте пару ключей Ed25519:

gen_keys.sh
#!/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 (для загрузки)."

Скачать gen_keys.sh

Затем создайте учётную запись подписанта и загрузите 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). Локальное хранилище вынесите на постоянный том:

docker-compose.yml
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. Префикс — это идентичность узла: у каждого узла-подписанта должен быть свой префикс; один и тот же префикс допустим только у резервного экземпляра того же узла (см. предупреждение ниже).

docker-compose.yml
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

Перенос не требует новой привязки — данные копируются как есть:

  1. Остановите узел.
  2. Выполните разовый запуск приложения co-signer с флагом --migrate-store, добавив переменные STORE_TYPE=s3 и STORE_S3_*: все ключи скопируются в бакет, после чего процесс завершится. В непустой префикс миграция не запускается.
  3. Запустите узел с STORE_TYPE=s3. Старый файл хранилища сохраните как резервную копию для отката.

Копирование выполняется целиком или не выполняется вовсе — возобновить прерванную миграцию нельзя. Если она оборвалась (сеть, истёкшие креды), процесс завершится с ошибкой, успев записать часть ключей, и повторный запуск упрётся в отказ по непустому префиксу. Сколько ключей успело скопироваться, видно в логе — по полю keys_copied строки об ошибке миграции.

Как повторить:

  1. Не запускайте узел с STORE_TYPE=s3 в этом состоянии — в бакете лежит неполная копия. Исходный файл хранилища открывается только на чтение и не изменяется, поэтому актуальные данные всё ещё в нём.
  2. Удалите незавершённую копию — объекты под <префикс>/kv/. Соседний объект <префикс>/lease не трогайте, если этот же префикс использует другой узел.
  3. Повторите запуск с --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:

pair_device_token.sh
#!/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"

Скачать pair_device_token.sh

Запустите скрипт, передав параметры через флаги; путь к приватному ключу задаётся --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. Со стороны оператора никаких действий не требуется — после обновления узел перезапускается автоматически, а в консоли обновляется поле Версия.

Отвязка

Чтобы отключить узел от рабочего пространства, в разделе Косайнеры откройте меню строки и выберите Отвязать. После этого узел теряет доступ к этому рабочему пространству и перестаёт получать запросы на подпись. Отвязка действует только на текущее рабочее пространство — в других узел продолжает работать.