INTERNAL OPERATIONS GUIDE · 01

Документация

Настройка API-пользователей

Пошаговая инструкция для администратора Vault: от Userpass mount до проверки первого JWT через наше API.

Важно

Страница статическая: команды только копируются и не выполняются автоматически. Реальные credentials здесь не хранятся.

00 / CONTEXT

Два независимых доступа к Vault

Credentials приложения и внешнего API-пользователя никогда не смешиваются.

A

Доступ приложения

Node.js application
        |
        | VAULT_ROLE_ID + VAULT_SECRET_ID
        v
Vault AppRole
        |
        v
Application Vault token
        |
        v
Internal application operations
  • чтение секретов;
  • JWT signing secret;
  • внутренние операции и health.
B

Доступ API-пользователя

External API user
        |
        | username + password
        v
POST /api/v1/auth/token
        |
        v
Vault userpass: api-users
        |
        v
temporary user's Vault token
        |
        v
Node.js issues application JWT
  • backend отзывает временный Vault token;
  • клиент получает только JWT приложения.
Внимание

Пользователю API НЕ выдаются VAULT_ROLE_ID и VAULT_SECRET_ID приложения.
Приложение НЕ использует свой AppRole для проверки пароля внешнего пользователя.
Регистрация выполняется через публичный POST /api/v1/auth/register, а активация — вручную в Vault.

01 / CONTEXT

Где выполнять команды

В текущем Compose сервис Vault называется vault. Выполняйте команды из каталога docker:

cd docker
docker compose --profile vault exec vault vault status

Если вы уже внутри Vault container:

vault status
Проверка

Vault CLI уже есть внутри текущего контейнера; ставить CLI на host не требуется.

02 / PREFLIGHT

Предварительная проверка Vault

Состояние

vault status
InitializedtrueSealedfalse

Для работы Vault должен быть initialized и unsealed. Unseal keys не публикуются.

Auth methods

vault auth list

Ожидается api-users/ с типом userpass.

03 / BOOTSTRAP

Первичная настройка api-users

Важно

Выполняется один раз. Если api-users/ уже есть в vault auth list, enable повторно не выполняйте.

vault auth enable -path=api-users userpass
vault auth list

Не выполняйте vault auth disable api-users/ на рабочем Vault: это удаляет mount вместе с пользователями.

04 / NAMING

Правила username

Используйте letters, digits, _, - и .. Имя не начинается с - или . и не заканчивается ..

api-client-1accounting-servicecrm.integrationpartner_01

Не используйте @, /, пробелы и path-like usernames.

05 / AUTHORIZATION

Policies для API-пользователей

Сейчас policies из Vault переносятся в JWT claim policies. Middleware проверяет подпись, issuer, audience и exp; обязательная проверка конкретного policy name в текущем API не подключена.

Минимальная policy api-v1 не даёт доступа к application secrets:

vault policy write api-v1 - <<'EOF'
path "auth/token/revoke-self" {
  capabilities = ["update"]
}
EOF

vault policy read api-v1
06 / USERS

Создание нового API-пользователя

1. Имя

export API_USERNAME="api-client-1"

2. Пароль без shell history

read -r -s -p "Password: " API_PASSWORD
echo

3. Создание

vault write "auth/api-users/users/${API_USERNAME}" \
  password="${API_PASSWORD}" \
  token_policies="api-v1" \
  token_ttl="5m" \
  token_max_ttl="15m"

4. Очистка и проверка

unset API_PASSWORD
vault read "auth/api-users/users/${API_USERNAME}"

Password Vault обратно не возвращает. Проверяйте policies, token_ttl и token_max_ttl.

07 / LIFECYCLE

Управление пользователем

Список и настройки

vault list auth/api-users/users
export API_USERNAME="api-client-1"
vault read "auth/api-users/users/${API_USERNAME}"

Смена пароля

export API_USERNAME="api-client-1"
read -r -s -p "New password: " API_PASSWORD
echo
vault write "auth/api-users/users/${API_USERNAME}/password" \
  password="${API_PASSWORD}"
unset API_PASSWORD
Важно

Старый пароль перестаёт работать для новой authentication, но уже выданные JWT действуют до exp.

Policies

export API_USERNAME="api-client-1"
vault write "auth/api-users/users/${API_USERNAME}/policies" token_policies="api-v1"
vault write "auth/api-users/users/${API_USERNAME}/policies" token_policies="api-v1,another-policy"
vault read "auth/api-users/users/${API_USERNAME}"

Новые policies попадут только в новые authentication/JWT.

TTL

export API_USERNAME="api-client-1"
vault write "auth/api-users/users/${API_USERNAME}" token_ttl="5m" token_max_ttl="15m"

Password повторно задавать не требуется.

Удаление

export API_USERNAME="api-client-1"
vault delete "auth/api-users/users/${API_USERNAME}"
vault list auth/api-users/users
Внимание

Удаление запрещает новые авторизации, но не отзывает уже выпущенные JWT до их exp. Blacklist в проекте не подключён.

08 / VERIFY

Проверка доступа

Диагностический userpass login

Это операция администратора, не обычного API-клиента:

export API_USERNAME="api-client-1"
USER_VAULT_TOKEN="$(
  vault login -method=userpass -path=api-users -token-only -no-store \
    username="${API_USERNAME}"
)"
VAULT_TOKEN="${USER_VAULT_TOKEN}" vault token lookup
VAULT_TOKEN="${USER_VAULT_TOKEN}" vault token revoke -self
unset USER_VAULT_TOKEN
Внимание

Vault token нельзя передавать внешнему клиенту. Внешний клиент использует application JWT.

Основной API flow

export APP_URL="http://127.0.0.1:3000"
export API_USERNAME="api-client-1"
read -r -s -p "Password: " API_PASSWORD
echo
AUTH_RESPONSE="$(
  jq -n --arg username "${API_USERNAME}" --arg password "${API_PASSWORD}" \
    '{username:$username,password:$password}' |
  curl -sS -X POST -H 'Content-Type: application/json' --data-binary @- \
    "${APP_URL}/api/v1/auth/token"
)"
unset API_PASSWORD
echo "${AUTH_RESPONSE}" | jq
ACCESS_TOKEN="$(echo "${AUTH_RESPONSE}" | jq -r '.access_token')"
curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" "${APP_URL}/api/v1/me" | jq
unset ACCESS_TOKEN AUTH_RESPONSE
Результат

Внешний клиент получает JWT приложения, не Vault token.

09 / RUNBOOK

Типовой жизненный цикл

  1. Создать policy при необходимости.
  2. Создать user в api-users.
  3. Проверить vault read.
  4. Проверить POST /api/v1/auth/token.
  5. Проверить /api/v1/me.
  6. Передать клиенту только его credentials.
  7. При компрометации сменить password.
  8. При отзыве доступа удалить user.
Никогда

Не передавайте клиенту root/admin token, RoleID/SecretID AppRole, JWT signing secret или Vault user token.

10 / TROUBLESHOOTING

Типовые ошибки

vault: command not found

Используйте docker compose exec vault vault ....

connection refused

Проверьте контейнер, VAULT_ADDR, network и vault status.

Vault is sealed

Сначала выполните unseal; keys не публикуются.

path is already in use

Проверьте vault auth list; не удаляйте mount.

permission denied

ADMIN token не имеет прав. Не используйте AppRole приложения для администрирования.

invalid username or password

Проверьте credentials и vault list auth/api-users/users.

API 401

Credentials не прошли либо JWT invalid/expired.

API 503

Проверьте Vault и доступность backend → Vault.

11 / DASHBOARD

Права приложения для Dashboard и регистрации

AppRole Node.js использует минимальные права для списка, регистрации и rollback:

path "sys/auth" {
  capabilities = ["read"]
}
path "auth/api-users/users" {
  capabilities = ["list"]
}
path "auth/api-users/users/*" {
  capabilities = ["create"]
}
path "identity/entity" {
  capabilities = ["create"]
}
path "identity/entity-alias" {
  capabilities = ["create"]
}
path "identity/lookup/entity" {
  capabilities = ["update"]
}
path "identity/entity/id/*" {
  capabilities = ["delete"]
}

Регистрация создаёт disabled Entity, Alias и Userpass user; активация выполняется вручную через Vault UI/CLI. Право create не позволяет менять пароль существующего user. Список пользователей берётся из Vault — MySQL хранит только JWT-состояние.

AppRole приложения → внутренние операции приложения.
username/password api-users → аутентификация конкретного внешнего пользователя.

12 / JWT LIFECYCLE

Logout JWT

POST /api/v1/auth/exit с заголовком Authorization: Bearer <current-jwt> отзывает только текущий JWT. Он не меняет Vault password, не удаляет Vault user и не завершает другие JWT-сессии пользователя.

13 / SECURITY

Чек-лист безопасности

  • API users находятся в api-users, не в AppRole приложения.
  • AppRole используется только backend.
  • Не храните passwords в .env, MySQL или обычном KV.
  • Не вставляйте реальные passwords в shell commands — используйте read -s.
  • Не логируйте passwords, Authorization и Vault tokens.
  • Vault user token краткоживущий и отзывается backend после login.
  • JWT имеет короткий TTL и server-side session revocation.
  • Logout отзывает только текущий JWT.
BLOCKCHAIN BOUNDARY

TRON wallet architecture

Core отвечает только за API/Auth/Actions/Vault transport и orchestration. Вся TRON-specific логика находится в app/chains/tron/; TRON-код запрещено размещать в core services/controllers/actions, кроме вызова публичного интерфейса TRON-модуля.

Core
 ├─ API/Auth/Actions/Vault transport
 └─ chains/
     ├─ tron/       <- вся TRON-specific логика
     └─ ethereum/   <- будет добавлен позднее

Позднее app/chains/ethereum/ добавляется независимо от TRON и ядра.

Vault storage

tron-wallets/wallets/<TRON_ADDRESS> — KV v2 secret с CAS 0; policy AppRole:

path "tron-wallets/data/wallets/*" {
  capabilities = ["create", "read"]
}

Для signer не требуются update, delete, destroy или list. get_wallet_secrets возвращает критические секреты.

TRON MONITORING

Наблюдение, reconciliation и webhook

Core отвечает за API/JWT и orchestration; TRON-specific network, parsing, cursor и repositories находятся в app/chains/tron/. add_watch_wallet фиксирует confirmed head, baseline и новую session; active watch идемпотентен, reactivation начинает новую session и не восстанавливает transactions или balance changes inactive period. unwatch_wallet прекращает новые scans, но pending events сохраняются.

Два независимых cursor

Transaction ingestion читает confirmed blocks, локально сопоставляет TRX/официальный USDT со всеми active wallets и атомарно сохраняет transfers, reconciliation jobs и глобальный transaction cursor. Он никогда не запрашивает balance. При большом lag используется catch-up без overlap каждого batch; возле head — небольшой overlap с MySQL deduplication.

Balance reconciliation имеет постоянную MySQL-очередь и lease. Snapshot разрешён только после того, как transaction cursor догнал target job. Snapshot — текущее подтверждённое состояние, а не исторический баланс target block. Ошибки повторяются бесконечно с bounded backoff и не приводят к потере transfers.

Usage и workers

Production запускает один npm run worker:tron; TRON_EMBEDDED_WORKERS=false. Общий scheduler соблюдает RPS, приоритеты и учитывает каждый физический request по безопасному идентификатору ключа. Суточное окно задаётся TRON_API_DAILY_RESET_HOUR_UTC=16: лимит и прогноз сбрасываются ежедневно в 16:00 UTC. При 429 запрос сразу пробует следующий ключ, после блокировки всех ключей применяется максимальная пауза.

Immutable webhook

Payload новых batches содержит schema_version, монотонный sequence и batch_id. Успех — строго HTTP 200; семантика at-least-once требует deduplication по batch_id. После быстрых retries batch автоматически переходит в бесконечный slow retry и блокирует более новые sequence. Немедленный retry доступен кнопкой Dashboard и JWT endpoint POST /api/v1/tron/webhook_batches/{batch_uuid}/retry, не меняя payload, hash или состав.

HMAC остаётся HMAC-SHA256(timestamp + "." + exact_raw_json_body, TRON_WEBHOOK_SECRET). Retention удаляет chunks только delivered/completed данных. Dashboard и /health/monitoring показывают lag, balance queue, oldest webhook/blocking batch, daily usage и retention, не раскрывая keys, secret или payload.

Настройки

Все intervals, batch sizes, catch-up/overlap, reconciliation lease/retry, fast/slow webhook retry, RPS/daily budget/reset, retention и health thresholds перечислены с единицами в .env.example. Подробная внутренняя эксплуатационная документация: docs/tron-monitoring-internals.md.

WEBHOOK SECURITY

Проверка подписи на PHP

TRON_WEBHOOK_SECRET — общий секрет Core и webhook-получателя. Он доказывает, что запрос сформирован доверенной стороной и raw JSON body не изменился. Создайте случайное значение командой openssl rand -hex 32, настройте одинаковое значение на обеих сторонах и не сохраняйте его в репозитории, payload или логах.

Важно

Подпись нужно вычислять по исходной строке из php://input до json_decode(). Повторная сериализация JSON изменит байты и подпись не совпадёт.

<?php
$secret = getenv('TRON_WEBHOOK_SECRET') ?: '';
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$batchId = $_SERVER['HTTP_X_WEBHOOK_BATCH_ID'] ?? '';

if ($secret === '' || !ctype_digit($timestamp)) {
    http_response_code(401);
    exit('Invalid webhook configuration or timestamp');
}

// Защита от повторного использования старой подписи: допустимое окно 5 минут.
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit('Expired webhook timestamp');
}

$expected = 'sha256=' . hash_hmac(
    'sha256',
    $timestamp . '.' . $rawBody,
    $secret
);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid webhook signature');
}

$payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
if (($payload['batch_id'] ?? '') !== $batchId || $batchId === '') {
    http_response_code(400);
    exit('Invalid batch id');
}

// Проверить в своей БД, не обработан ли уже $batchId.
// Сохранить batch_id и бизнес-изменения в одной локальной транзакции.
// Повтор того же batch_id должен завершаться без повторного применения событий.

http_response_code(200); // Только HTTP 200 подтверждает доставку для Core.
header('Content-Type: application/json');
echo json_encode(['accepted' => true, 'batch_id' => $batchId]);

Core использует at-least-once delivery: если HTTP 200 потеряется в сети, тот же immutable batch будет отправлен повторно. Поэтому получатель должен хранить обработанные batch_id и отвечать HTTP 200 также на уже успешно обработанный duplicate.