Ключ 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"]
Ротация ключа без остановки воркеров
- Создайте новый API-ключ в панели управления CaptchaAI.
- Запишите его в Vault:
vault kv put secret/captchaai api_key="NEW_KEY". - Дождитесь, пока воркеры перечитают значение на очередном цикле обновления — при интервале в 3600 с это максимум час.
- Отзовите старый ключ в панели управления, когда убедитесь, что все воркеры перешли на новый.
Ни изменений в коде, ни развёртывания на этом пути нет. При регулярной ротации сократите интервал обновления до 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 недоступен в момент обновления?
Оставьте последнее полученное значение в памяти, залогируйте неудачную попытку и продолжайте работать на кэше. Аварийное завершение воркера из-за недоступности хранилища секретов — худший из вариантов: очередь задач встанет целиком.
Один ключ на всех воркеров или по ключу на процесс?
Одного достаточно: лимит задаётся числом потоков тарифа, а не количеством ключей. Отдельные ключи заводят ради изоляции сред и точечного отзыва доступа, а не ради производительности.