Tích Hợp

Tích hợp Vault để quản lý khóa API CaptchaAI

Khóa API hiếm khi rò rỉ từ chỗ bạn nghĩ: nó thoát ra qua một layer Docker cũ, một file .env commit nhầm, hay ảnh chụp màn hình dán vào group chat. Cách chặn cả ba là để worker không giữ sẵn khóa nào — nó đăng nhập HashiCorp Vault bằng danh tính của chính nó rồi đọc khóa CaptchaAI lúc runtime.

Đổi khóa từ đó chỉ còn một lệnh vault kv put. Năm bước dưới đây dựng đúng luồng ấy.

Cần gì trước khi bắt đầu

Thành phần Yêu cầu
Vault server Self-hosted hoặc HCP Vault
Quyền truy cập Vault CLI hoặc HTTP API
Tài khoản CaptchaAI Một API key đang hoạt động
Runtime Python 3.8+ hoặc Node.js 18+

Bước 1: ghi khóa API vào KV v2

Bật secrets engine KV v2 rồi đặt khóa vào path secret/captchaai:

# 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

Lệnh vault kv get chỉ để tự xác nhận. Đừng đưa nó vào script CI — khóa sẽ nằm nguyên trong log build.

Bước 2: cấp policy read-only cho worker

Worker chỉ cần đọc, không cần create hay update:

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

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

Nạp policy vào Vault:

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

Để ý mảnh data/: KV v2 dùng đường dẫn API khác đường dẫn CLI, thiếu nó là ra 403 Forbidden ngay lần chạy đầu.

Bước 3: worker Python đọc khóa lúc runtime

Đoạn dưới đọc khóa từ Vault, giữ trong memory, tự lấy lại mỗi giờ, rồi gửi task reCAPTCHA v2 và polling res.php:

# 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]}...")

_refresh_key_if_needed() chạy trước mỗi lần giải, nên khóa mới ghi vào Vault sẽ tới worker trong tối đa một chu kỳ refresh, không cần restart.

Bước 4: worker Node.js đọc khóa lúc runtime

Cùng mô hình cho worker JavaScript, khác ở chỗ gọi thẳng HTTP API của Vault:

// 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)}...`);
})();

Bẫy duy nhất: init() là async, phải await trước lần giải đầu tiên, nếu không this.apiKey vẫn là null.

Bước 5: thay token tĩnh bằng AppRole

VAULT_TOKEN tiện trên máy dev nhưng có TTL — hết hạn giữa đêm là cả cụm worker đứng im. Chọn theo nơi worker chạy:

Phương thức Phù hợp với Cấu hình
Token Máy dev, CI/CD VAULT_TOKEN
AppRole Service production Role ID + Secret ID
Kubernetes Workload K8s JWT service account
AWS IAM EC2/Lambda Instance role

AppRole cho production

# 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"]

Hai giá trị này không cùng mức nhạy cảm:

  • Role ID — coi như tên đăng nhập, để trong config hay biến môi trường đều được.
  • Secret ID — phần thật sự nhạy cảm: inject lúc khởi động container, đừng bake vào image.

Xoay khóa API CaptchaAI mà không deploy lại

Quy trình xoay khóa gọn lại còn bốn bước:

  1. Tạo khóa API mới trong dashboard CaptchaAI
  2. Ghi đè: vault kv put secret/captchaai api_key="NEW_KEY"
  3. Chờ hết một chu kỳ refresh — worker tự nhận khóa mới
  4. Thu hồi khóa cũ sau khi mọi worker đã refresh

Không sửa code, không build lại image, không rolling restart. Cho _key_refresh_interval đọc từ biến môi trường để lúc cần xoay gấp còn hạ xuống 300 giây.

Bốn lỗi hay gặp lúc chạy thật

Vấn đề Nguyên nhân Cách xử lý
403 Forbidden từ Vault Policy thiếu path Kiểm tra secret/data/captchaai
VAULT_TOKEN hết hạn Token tĩnh vượt TTL Chuyển sang AppRole
Worker vẫn dùng khóa cũ Refresh quá thưa Hạ _key_refresh_interval
Không kết nối được Vault Mạng lỗi hoặc Vault seal Dùng khóa đã cache, ghi log

Bạn được gì sau năm bước trên

Trước khi có Vault Sau khi có Vault
Khóa nằm trong .env hoặc trong code Khóa lưu mã hóa trong Vault
Khóa chuyền tay qua Slack, email Chỉ đọc qua API đã xác thực
Không biết ai đã dùng khóa Mỗi lần đọc ghi kèm danh tính
Một khóa cho mọi môi trường Mỗi môi trường một policy

Tách policy: bài toán của team outsourcing

Một agency ở TP.HCM chạy chung cụm worker cho năm khách hàng bằng đúng một API key. Khách hàng A hỏi tháng trước dự án của họ tiêu bao nhiêu thread thì không ai trả lời được.

Tách secret/captchaai/<project>, mỗi project một policy và một AppRole, thì audit device ghi rõ danh tính nào đọc khóa nào, lúc nào. Với pipeline theo dõi giá công khai trên Shopee hay Lazada, nếu có chạm dữ liệu cá nhân, Nghị định 13/2023/NĐ-CP là thêm một lý do để cuốn log đó có sẵn từ đầu.

Chi phí CaptchaAI có đổi không

Không. CaptchaAI tính theo thread (luồng giải đồng thời) chứ không theo từng lần giải: BASIC ($15/tháng, 5 thread) đủ cho team QA nhỏ, ADVANCE ($90/tháng, 50 thread) hợp với pipeline scraping chạy liên tục. Giá bằng USD; tách path trong Vault không phát sinh thêm khoản nào.

Cùng khóa đó chạy được cho những loại nào:

  • Hỗ trợ đầy đủ: reCAPTCHA v2/v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, image/OCR, grid-image, BLS CAPTCHA
  • Đang ở bản beta: CaptchaFox, Friendly Captcha, Lemin
  • Chưa hỗ trợ: hCaptcha và FunCaptcha; GeeTest v4 sắp ra mắt

Câu hỏi thường gặp

Vault seal lúc nửa đêm thì worker có dừng giải CAPTCHA không?

Không, nếu khóa đã nằm trong memory như code ở Bước 3. Worker dùng tiếp khóa cũ; chỉ tắc khi có worker restart đúng lúc Vault chưa unseal.

Đặt _key_refresh_interval bao nhiêu là hợp lý?

3600 giây là mặc định an toàn. Hạ xuống 300 giây trong đợt xoay khóa, thấp hơn nữa chỉ tăng tải lên Vault.

Chạy trên Kubernetes thì có cần AppRole không?

Không. Dùng Kubernetes auth method: pod trình JWT của service account, Vault đổi lấy token đúng policy, và bạn bớt hẳn việc xoay Secret ID.

Bước tiếp theo

  1. Lấy API key CaptchaAI rồi vault kv put vào secret/captchaai
  2. Nạp policy read-only và tạo AppRole cho worker
  3. Chạy lại pipeline — repo không còn giữ khóa

Cùng chủ đề bảo mật khóa

Nếu chưa sẵn sàng dựng Vault

Os comentários estão desativados para este artigo.