Integrations

Интеграция HashiCorp Vault для хранения API-ключа CaptchaAI

Ключ CaptchaAI, лежащий в .env на пяти машинах, рано или поздно попадает в git, в чат или в лог сборки. Vault убирает эту цепочку целиком: ключ хранится в зашифрованном виде, воркер получает его во время выполнения по токену, а каждое обращение записывается в журнал с указанием, кто именно читал секрет.

Разница видна в момент замены ключа. Без Vault это правка переменных окружения, пересборка образов и раскатка всех воркеров; с Vault — одна команда, после которой воркеры подхватывают новое значение на очередном цикле обновления.

Ниже — рабочая схема: KV-хранилище, политика только на чтение, клиенты на Python и Node.js, аутентификация через AppRole и порядок ротации. Если первый запрос к API вы ещё не делали, начните с материала быстрый старт CaptchaAI: обращение к CaptchaAI в примерах ниже — это обычный HTTP-запрос, а Vault отвечает только за то, откуда берётся значение ключа.

Что меняется, когда ключ CaptchaAI лежит в Vault

Сравнение по пяти пунктам, которые чаще всего всплывают на код-ревью и при аудите:

Без Vault С Vault
Ключ лежит в .env или прямо в коде Ключ хранится в Vault в зашифрованном виде
Ключ пересылают в Slack или почтой Ключ выдаётся только по аутентифицированному запросу
Кто и когда читал ключ — неизвестно Каждое чтение попадает в журнал аудита с идентификатором
Ротация вручную, с пересборкой воркеров Ротация одной командой, без пересборки
Один ключ на все среды Свой ключ на каждую среду, со своей политикой

Отдельного внимания заслуживает журнал обращений: он показывает, кто и когда читал учётные данные сервиса, без дополнительной обвязки. Для команд, работающих с персональными данными в контуре 152-ФЗ, это привычное требование внутренней политики.

Что понадобится перед началом

  • Сервер HashiCorp Vault — собственный или HCP Vault
  • Доступ к Vault CLI или к его HTTP API
  • API-ключ CaptchaAI и активный тариф
  • Python 3.8+ или Node.js 18+

Шаг 1. Положите API-ключ CaptchaAI в KV-хранилище

Движок KV версии 2 хранит историю значений, поэтому предыдущий ключ остаётся доступным до явного удаления — удобно при откате, если новый ключ не заработал.

# Enable the KV secrets engine (if not already enabled)
vault secrets enable -path=secret kv-v2

# Store the CaptchaAI API key
vault kv put secret/captchaai api_key="YOUR_API_KEY"

# Verify
vault kv get secret/captchaai

Для разных сред заведите разные пути: secret/captchaai/dev, secret/captchaai/staging, secret/captchaai/prod. Один путь на все среды — самая частая ошибка на старте, из-за которой тестовый прогон случайно расходует потоки рабочего тарифа.

Шаг 2. Ограничьте воркеров политикой только на чтение

Воркеру, который решает CAPTCHA, не нужны права на запись. Дайте ему ровно два разрешения — чтение данных и чтение метаданных:

# captcha-worker-policy.hcl
path "secret/data/captchaai" {
  capabilities = ["read"]
}

path "secret/metadata/captchaai" {
  capabilities = ["read"]
}

Примените политику:

vault policy write captcha-worker captcha-worker-policy.hcl

Обратите внимание на путь secret/data/... — в KV v2 сегмент data обязателен, и политика, написанная как secret/captchaai, тихо не сработает. Именно отсюда растёт большинство ошибок 403 Forbidden в первые часы после подключения.

Шаг 3. Читайте ключ из Vault в Python

Клиент hvac забирает секрет при создании объекта и перечитывает его по таймеру. Интервал в 3600 с — разумная отправная точка: ротация доезжает до воркеров в пределах часа, а Vault не получает лишних запросов.

# vault_solver.py
import os
import time
import hvac
import requests

# Connect to Vault
vault_client = hvac.Client(
    url=os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200"),
    token=os.environ.get("VAULT_TOKEN"),
)

def get_api_key():
    """Retrieve CaptchaAI API key from Vault."""
    secret = vault_client.secrets.kv.v2.read_secret_version(
        path="captchaai",
        mount_point="secret",
    )
    return secret["data"]["data"]["api_key"]

class CaptchaSolver:
    """CAPTCHA solver with Vault-managed credentials."""

    def __init__(self):
        self.api_key = get_api_key()
        self.session = requests.Session()
        self._key_fetched_at = time.time()
        self._key_refresh_interval = 3600  # Re-fetch key hourly

    def _refresh_key_if_needed(self):
        """Periodically refresh the key from Vault."""
        if time.time() - self._key_fetched_at > self._key_refresh_interval:
            self.api_key = get_api_key()
            self._key_fetched_at = time.time()

    def solve(self, sitekey, pageurl):
        """Solve reCAPTCHA v2 using Vault-managed key."""
        self._refresh_key_if_needed()

        # Submit
        resp = self.session.get("https://ocr.captchaai.com/in.php", params={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": "1",
        })
        result = resp.json()

        if result.get("status") != 1:
            raise Exception(f"Submit failed: {result.get('request')}")

        task_id = result["request"]
        time.sleep(15)

        for _ in range(25):
            poll = self.session.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": "1",
            })
            poll_result = poll.json()

            if poll_result.get("status") == 1:
                return poll_result["request"]
            if poll_result.get("request") != "CAPCHA_NOT_READY":
                raise Exception(f"Error: {poll_result.get('request')}")

            time.sleep(5)

        raise Exception("Timeout")

# Usage
solver = CaptchaSolver()
token = solver.solve(
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    "https://www.google.com/recaptcha/api2/demo"
)
print(f"Token: {token[:30]}...")

Схема работы с API стандартная: отправка задачи в in.php, затем опрос res.php до появления токена — разбор по шагам есть в руководстве как решить reCAPTCHA v2 через API. Vault здесь лишь подставляет значение key.

Шаг 4. То же самое в Node.js

В Node.js отдельный клиент не нужен — Vault отдаёт секрет обычным GET-запросом с заголовком X-Vault-Token, поэтому хватает axios. Единственная особенность: у KV v2 ответ вложен дважды, отсюда resp.data.data.data.api_key.

// vault_solver.js
const axios = require('axios');

const VAULT_ADDR = process.env.VAULT_ADDR || 'http://127.0.0.1:8200';
const VAULT_TOKEN = process.env.VAULT_TOKEN;

async function getApiKey() {
  const resp = await axios.get(
    `${VAULT_ADDR}/v1/secret/data/captchaai`,
    { headers: { 'X-Vault-Token': VAULT_TOKEN } }
  );
  return resp.data.data.data.api_key;
}

class CaptchaSolver {
  constructor() {
    this.apiKey = null;
    this.keyFetchedAt = 0;
    this.refreshInterval = 3600000; // 1 hour
  }

  async init() {
    this.apiKey = await getApiKey();
    this.keyFetchedAt = Date.now();
  }

  async refreshKeyIfNeeded() {
    if (Date.now() - this.keyFetchedAt > this.refreshInterval) {
      this.apiKey = await getApiKey();
      this.keyFetchedAt = Date.now();
    }
  }

  async solve(sitekey, pageurl) {
    await this.refreshKeyIfNeeded();

    const submit = await axios.get('https://ocr.captchaai.com/in.php', {
      params: {
        key: this.apiKey, method: 'userrecaptcha',
        googlekey: sitekey, pageurl, json: '1',
      },
    });

    if (submit.data.status !== 1) throw new Error(submit.data.request);
    const taskId = submit.data.request;

    await new Promise(r => setTimeout(r, 15000));

    for (let i = 0; i < 25; i++) {
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: this.apiKey, action: 'get', id: taskId, json: '1' },
      });

      if (poll.data.status === 1) return poll.data.request;
      if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
      await new Promise(r => setTimeout(r, 5000));
    }
    throw new Error('Timeout');
  }
}

(async () => {
  const solver = new CaptchaSolver();
  await solver.init();

  const token = await solver.solve(
    '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
    'https://www.google.com/recaptcha/api2/demo'
  );
  console.log(`Token: ${token.slice(0, 30)}...`);
})();

Тот же приём переносится на другие типы проверок без изменений в части Vault — например, на решение Cloudflare Turnstile через API или на GeeTest v3 через API. Меняется только метод и набор параметров запроса.

Как воркеры аутентифицируются в Vault

Метод Когда подходит Что настраивается
Token Локальная разработка, CI/CD Переменная окружения VAULT_TOKEN
AppRole Рабочие сервисы Role ID + Secret ID
Kubernetes Нагрузки в кластере K8s JWT сервисного аккаунта
AWS IAM Воркеры на EC2 и в Lambda Роль экземпляра

Статический VAULT_TOKEN в переменной окружения — это тот же секрет в .env, только уровнем выше. Для постоянно работающих воркеров используйте AppRole: Role ID можно держать в конфигурации, а Secret ID выдаётся с ограниченным сроком жизни и продлевается автоматически.

Пример AppRole для рабочей среды

# AppRole authentication — no static token needed
vault_client = hvac.Client(url=os.environ["VAULT_ADDR"])
vault_client.auth.approle.login(
    role_id=os.environ["VAULT_ROLE_ID"],
    secret_id=os.environ["VAULT_SECRET_ID"],
)

# Now read the secret
secret = vault_client.secrets.kv.v2.read_secret_version(path="captchaai")
api_key = secret["data"]["data"]["api_key"]

Ротация ключа без остановки воркеров

  1. Создайте новый API-ключ в панели управления CaptchaAI.
  2. Запишите его в Vault: vault kv put secret/captchaai api_key="NEW_KEY".
  3. Дождитесь, пока воркеры перечитают значение на очередном цикле обновления — при интервале в 3600 с это максимум час.
  4. Отзовите старый ключ в панели управления, когда убедитесь, что все воркеры перешли на новый.

Ни изменений в коде, ни развёртывания на этом пути нет. При регулярной ротации сократите интервал обновления до 900 с — окно перехода станет вчетверо короче.

Сценарий: агентство с тремя средами

Типичный случай для команды из Алматы, Минска или Тбилиси, которая обслуживает несколько клиентов: разработка идёт на тарифе BASIC ($15/мес, 5 потоков), а продакшен-парсинг — на ADVANCE ($90/мес, 50 потоков). Тарификация у CaptchaAI идёт по количеству одновременных потоков, а не по числу решений, поэтому предсказуемый ежемесячный платёж в долларах легко закладывается в смету клиента.

Проблема начинается, когда оба ключа лежат в общем .env: прогон на CI забирает потоки рабочего тарифа, и продакшен встаёт в очередь. Разные пути Vault (secret/captchaai/dev и secret/captchaai/prod) с отдельными политиками решают это структурно — CI просто не может прочитать рабочий ключ. При передаче проекта подрядчику достаточно отозвать AppRole.

Диагностика частых ошибок

Симптом Причина Что сделать
403 Forbidden от Vault Политика не даёт права на чтение Проверьте путь secret/data/... в captcha-worker-policy.hcl
VAULT_TOKEN перестал работать Истёк срок жизни токена Перейдите на AppRole с автопродлением
Воркер держит старый ключ Слишком длинный интервал обновления Уменьшите _key_refresh_interval
Vault недоступен Сетевая ошибка или недоступный сервер Кэшируйте ключ в памяти и работайте на кэше до восстановления
KeyError: 'data' в Python Секрет создан в KV v1, а читается как v2 Проверьте версию движка: vault secrets list -detailed

Частые вопросы

Какой интервал обновления ключа выбрать?

От 900 до 3600 с для постоянно работающих воркеров. Более частый опрос Vault смысла не имеет: ключ меняется редко, а каждый лишний запрос попадает в журнал аудита и раздувает его.

Замедляет ли Vault решение CAPTCHA?

Нет. Ключ читается один раз за цикл обновления и лежит в памяти процесса, поэтому в самом запросе к in.php никакого дополнительного обращения к Vault не происходит. Время решения определяется типом проверки, а не способом хранения ключа.

Что делать, если Vault недоступен в момент обновления?

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

Один ключ на всех воркеров или по ключу на процесс?

Одного достаточно: лимит задаётся числом потоков тарифа, а не количеством ключей. Отдельные ключи заводят ради изоляции сред и точечного отзыва доступа, а не ради производительности.


Следующие шаги

Комментарии для этой статьи отключены.