Integrationen

Vault-Integration für die CaptchaAI-API-Schlüsselverwaltung

Der CaptchaAI-API-Schlüssel gehört als KV-v2-Secret in HashiCorp Vault: Jeder Solver-Worker erhält eine schreibgeschützte Policy, und der Schlüssel wandert erst zur Laufzeit in den Prozess – nie in ein Repository, nie in ein Image. Eine Rotation ist danach ein vault kv put statt eines Deployments.

Wer CAPTCHA-Worker auf Hetzner- oder netcup-VMs betreibt und aus GitLab CI heraus deployt, muss im Zweifel belegen können, welcher Dienst wann auf welches Secret zugegriffen hat. Vault bringt dieses Audit-Log mit. Dieser Leitfaden zeigt die Integration mit CaptchaAI als Solver-Backend, in Python und Node.js.

Vault statt .env: was sich praktisch ändert

Ohne Vault Mit Vault
Schlüssel steht in .env, im Image oder im Code Schlüssel liegt verschlüsselt im KV-v2-Store
Weitergabe per Slack, Ticket oder E-Mail Abruf über eine authentifizierte API, pro Dienst
Kein Nachweis, wer den Schlüssel gelesen hat Jeder Lesevorgang mit Identität und Zeitstempel
Ein Schlüssel für Entwicklung, Staging, Produktion Getrennte Pfade und Policies je Umgebung
Rotation heißt: alle Worker neu ausrollen Rotation ist ein Schreibvorgang, Worker ziehen nach

Wann sich der Aufwand lohnt

Für ein einzelnes Skript genügt eine Umgebungsvariable – wie das sauber aussieht, steht im Leitfaden zu Zugangsdaten in Umgebungsvariablen. Vault lohnt sich, sobald mehrere Personen und Maschinen denselben Schlüssel brauchen.

Ein Beispiel aus dem Agenturalltag: Ein Team in Wien betreut fünf Kundenprojekte über denselben CaptchaAI-Zugang. Der Schlüssel steckt in fünf .env-Dateien, in zwei GitLab-CI-Variablen und – historisch gewachsen – in einem Docker-Image auf einer alten VM. Verlässt eine Entwicklerin das Team, kann niemand sagen, welche Kopien noch aktiv sind.

Dazu kommt ein wirtschaftliches Argument: CaptchaAI rechnet Thread-basiert ab – BASIC (15 $/Monat, 5 Threads) bis VIP-3 (7.500 $/Monat, 5.000 Threads), mit unbegrenzten Lösungen je Thread. Ein breit gestreuter Schlüssel heißt: Beliebige Prozesse belegen Ihr Thread-Kontingent. Zugriffskontrolle ist hier zugleich Kapazitätskontrolle.

Voraussetzungen

  • Vault-Server, selbst gehostet oder als HCP Vault
  • Zugriff über Vault-CLI oder HTTP-API
  • Ein CaptchaAI-API-Schlüssel aus dem Dashboard
  • Python 3.8+ oder Node.js 18+

Schritt 1: Den API-Schlüssel als KV-v2-Secret ablegen

# 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

Der Pfad secret/captchaai ist bewusst flach. Sobald mehrere Umgebungen ins Spiel kommen, trennen Sie sauber: secret/captchaai/dev, secret/captchaai/staging, secret/captchaai/prod – jede mit eigenem Schlüssel und eigener Policy.

Schritt 2: Eine Policy nach dem Least-Privilege-Prinzip

Solver-Worker müssen den Schlüssel lesen, sonst nichts:

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

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

Wichtig ist der zweite Block: Bei KV v2 liegen Werte unter secret/data/..., die Versionshistorie unter secret/metadata/.... Wer nur den ersten Pfad freigibt, bekommt beim Lesen ein 403 Forbidden – der häufigste Stolperstein bei der Einrichtung. Danach registrieren Sie die Policy:

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

Schritt 3: Python-Worker mit Vault-Anbindung

Der Worker holt den Schlüssel beim Start aus Vault, hält ihn im Speicher und lädt ihn stündlich neu. Dazwischen läuft der übliche Ablauf: an in.php übermitteln, Task-ID entgegennehmen, res.php abfragen, bis das Token vorliegt.

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

Zwei Details verdienen Aufmerksamkeit. Das Refresh-Intervall von 3.600 Sekunden entkoppelt die Rotation vom Deployment, kostet aber bis zu einer Stunde, bis ein zurückgezogener Schlüssel überall verschwunden ist; 15 bis 60 Minuten sind meist ein guter Kompromiss. Und die Trennung von Abruf und Nutzung – get_api_key() kennt nur Vault, solve() nur die CaptchaAI-API – macht den Tausch des Secrets-Backends später trivial.

Schritt 4: Dieselbe Logik in Node.js

Für Node.js gilt dasselbe Muster, hier über die HTTP-API von Vault statt über eine Client-Bibliothek:

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

Achten Sie darauf, dass init() vor dem ersten solve() durchgelaufen ist – sonst geht die Anfrage mit leerem Schlüssel an die API und Sie sehen ERROR_WRONG_USER_KEY statt einer Task-ID.

Auth-Methoden: welche passt zu welchem Worker?

Wie sich ein Worker gegenüber Vault ausweist, hängt davon ab, wo er läuft:

Methode Passt zu Was Sie dafür brauchen
Token lokale Entwicklung, kurze CI-Jobs VAULT_TOKEN als Umgebungsvariable
AppRole dauerhaft laufende Solver-Worker Role-ID und Secret-ID
Kubernetes Worker als Pods im Cluster JWT des Service-Accounts
AWS IAM Worker auf EC2 oder in AWS Lambda Instanz- bzw. Ausführungsrolle

Statische Tokens sind bequem und genau deshalb riskant: Sie landen in denselben .env-Dateien, aus denen Sie den Schlüssel gerade herausgeholt haben.

AppRole im Produktivbetrieb

Bei AppRole meldet sich der Worker mit Role-ID und Secret-ID an und erhält ein kurzlebiges Token. Die Role-ID darf mit der Konfiguration ausgeliefert werden, die Secret-ID sollte kurz gültig und beim Start frisch ausgestellt sein:

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

Schlüsselrotation ohne Deployment

Die Reihenfolge ist entscheidend – erst schreiben, dann nachziehen lassen, dann zurückziehen:

  1. Neuen CaptchaAI-API-Schlüssel im Dashboard erzeugen
  2. Wert in Vault überschreiben: vault kv put secret/captchaai api_key="NEW_KEY"
  3. Die Worker übernehmen den neuen Wert beim nächsten Refresh-Zyklus automatisch
  4. Erst wenn alle Worker nachgezogen haben, den alten Schlüssel im Dashboard deaktivieren

Kein Codeeingriff, kein Rollout, kein Wartungsfenster. Planen Sie zwischen Schritt 3 und 4 mindestens ein volles Refresh-Intervall ein, besser zwei.

Wenn Vault kurzzeitig nicht erreichbar ist

Ein Secrets-Backend wird zum Single Point of Failure, sobald der Worker bei jedem Fehler abbricht. Der gezeigte Aufbau vermeidet das: Der Schlüssel liegt nach dem ersten Abruf im Prozessspeicher, ein fehlgeschlagener Refresh darf den bestehenden Wert stehen lassen. Protokollieren Sie den Fehlversuch und wiederholen Sie ihn mit exponentiellem Backoff. Eines sollten Sie dabei nie tun: den Schlüssel als Fallback auf die Platte schreiben.

Fehlerbehebung

Symptom Ursache Was hilft
403 Forbidden von Vault Die Policy deckt den Pfad nicht ab Bei KV v2 secret/data/captchaai und secret/metadata/captchaai freigeben
VAULT_TOKEN wird abgelehnt Token-TTL abgelaufen Auf AppRole umstellen und das Token erneuern lassen
Worker nutzt weiter den alten Schlüssel Refresh-Intervall zu lang _key_refresh_interval verkürzen oder Prozess neu starten
API meldet ERROR_KEY_DOES_NOT_EXIST Leerzeichen oder Zeilenumbruch im geschriebenen Wert Mit vault kv get secret/captchaai prüfen und sauber neu setzen

Häufige Fragen

Wie oft sollte ich den CaptchaAI-API-Schlüssel rotieren?

Quartalsweise als fester Rhythmus, zusätzlich anlassbezogen bei Personalwechseln und bei jedem Leak-Verdacht. Sobald die Rotation kein Deployment mehr auslöst, kostet ein zusätzlicher Durchlauf wenige Minuten.

Brauche ich dafür einen eigenen Vault-Server?

Nein. HCP Vault läuft mit demselben Code, nur VAULT_ADDR und die Auth-Methode unterscheiden sich. Selbst betreiben lohnt sich vor allem, wenn Ihre Infrastruktur ohnehin in Europa steht und Sie Zugriffsprotokolle im eigenen Haus auswerten wollen.

Was passiert mit laufenden Solve-Anfragen während der Rotation?

Nichts – eine übermittelte Aufgabe wird mit dem Schlüssel abgeschlossen, unter dem sie eingereicht wurde. Kritisch wird es erst, wenn Sie den alten Schlüssel deaktivieren, bevor alle Worker nachgezogen haben.

Wie verhindere ich, dass der Schlüssel in Logs auftaucht?

Nie vollständig ausgeben und in Debug-Ausgaben, Fehlerobjekten und Request-Dumps maskieren. Meist reicht eine kleine Hilfsfunktion, die beim Loggen alles bis auf die letzten vier Zeichen ersetzt.

Funktioniert das Muster auch mit anderen Secret-Stores?

Ja. Ob AWS Secrets Manager oder Azure Key Vault: Ausgetauscht wird nur get_api_key(). Das Prinzip – kein Schlüssel im Code, Abruf zur Laufzeit, Rotation ohne Rollout – bleibt gleich.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.