Skip to main content

Platform security

Recrut'Auto applies a defense in depth strategy across 5 independent layers, from the perimeter firewall to application-level validation. This page summarizes the protections your integrations (REST API, MCP, LinkedIn plugin) go through and the guarantees you can expect.

Layer 1 — Edge & IP intelligence

At the edge of our infrastructure, we filter known malicious IPs before any application processing:

  • CrowdSec Community Blocklist (~15,000 auto-updated IPs) — Shodan/Censys scanners, SSH brute-force bots, IPs flagged by the open-source CrowdSec community across all connected Security Engines.
  • Behavioral detectionhttp-bf, http-crawl-non_statics, http-probing, http-bad-user-agent scenarios continuously analyze logs and automatically ban IPs triggering attack patterns.
  • Kernel-level drop at the perimeter firewall: banned IPs are rejected before even reaching the application load balancer. Reaction latency: ~10 seconds between detection and effective ban.

Layer 2 — TLS & rate-limiting

All external communications use TLS 1.2+ with automatically renewed Let's Encrypt certificates. Internal certificates are issued via the DNS-01 challenge, without exposing any internal service on the Internet.

For rate-limiting, you are capped by default at:

SurfaceLimit
REST API /api/* (Bearer token)60 r/min per user
Admin surface20 r/s
Public endpoints (booking)10 r/s
MCP (Bearer PAT)30 r/s
Sentry tunnel30 r/s

Beyond that, you receive a 429 Too Many Requests. Implement an exponential backoff.

Layer 3 — Auth & ownership

All /api/* endpoints (except explicit public opt-ins like /api/calendars/public/*) require one of the following authentication modes:

  • Keycloak Bearer JWT (user logged in on the front end, OIDC PKCE).
  • Personal Access Token (Authorization: Bearer ra_<token>, see Tokens).
  • Scoped HS256-signed token (public booking 14 days, candidate image 10 minutes).

Once authenticated, each request is filtered by subscription ownership: you only see your subscription's resources, never those of another tenant. This rule is enforced at each backend service (get_subscription(db, current_user)), not only at the database level.

Sensitive non-user endpoints (LinkedIn reply analysis by our ML engine, etc.) are protected by rotatable static internal tokens, never exposed to the browser or the plugin.

Layer 4 — Application validation

  • Body cap — requests are rejected with 413 Payload Too Large beyond:
    • 10 MiB on the common /api/* trunk
    • 4 KiB on feature flags
    • 50 MiB on CV import
  • Transfer-Encoding sanity — requests with a malformed Transfer-Encoding header are rejected with 400 (request-smuggling mitigation).
  • Pydantic v2 strict mode on critical schemas (extra=forbid + max_length + pattern regex on IDs/slugs). A payload with an unexpected field is rejected with 422.
  • MIME validation on uploads (CV PDF, interview audio recordings) — verified via python-magic, not via the client-declared content-type.
  • ClamAV antivirus scan on all recruiter uploads (POST /api/import/cv, POST /api/import/csv, POST /api/interview/meets/{id}/recording). Fail-closed policy: if the clamd daemon is unreachable, the upload is rejected with 503 antivirus_unavailable rather than risking an unscanned file. A detected threat returns 422; the ClamAV signature name is logged internally but never returned to the client to prevent an attacker from iterating to find a bypass. Admins have a /security page to test the daemon (status card, test-file scan, EICAR button).

Layer 5 — Private data & assets

Candidate profile images (GET /api/candidates/{id}/image) are served via a short-lived signed URL mechanism:

  1. Your application calls GET /api/candidates/{id}/image-url with a Bearer token.
  2. The backend validates ownership and returns an audience-scoped HS256 JWT, valid for 10 minutes.
  3. The <img src=...?token=JWT> tag is consumed by the browser without re-authentication.

This authenticates the asset without exposing the Bearer token in an <img> request. Interview recordings (S3) follow the same pattern with presigned URLs.

Audit logging

All security events (auth gate hit, body cap exceeded, invalid signature, IP whitelist match) are emitted on the structured logger recrutauto.security and collected in Loki. Anomalies (an abnormal anonymous_warning rate, for example) are alerted via Sentry.

Common error codes

CodeMeaning
400Malformed request (suspicious Transfer-Encoding, invalid JSON)
401Token missing, expired, or invalid
402No active subscription — subscribe to a plan
403Insufficient permissions or IP banned at the perimeter
404Resource not found or outside your subscription's scope
413Body too large
422Pydantic validation failed (unknown field, invalid type, length exceeded)
429Rate limit exceeded

Integration best practices

  • Store Personal Access Tokens in a vault (HashiCorp Vault, AWS Secrets Manager, etc.), never in clear text in code.
  • Rotation: generate a new token every 90 days and revoke the old one after switching.
  • Scope minimally: if your integration only reads campaigns, avoid using an admin token.
  • Back off quickly on 429 — a polite client avoids being mistaken for a scraper and banned at the perimeter.
  • Verify the TLS certificate on the client side. No Recrut'Auto endpoint should be called over unencrypted HTTP.

Report a vulnerability

If you discover a security flaw, contact security@recrutauto.fr (PGP key available on request). We commit to acknowledging receipt within 24 business hours and to coordinating a responsible disclosure.

tip

This page describes the behavior observable from your integrations. The full internal architecture (components, ops runbooks) is documented separately for RecrutAuto teams.