---
title: "Platform Security"
description: "Two-factor authentication, encryption, VPC inference, and security scanning"
canonical: "https://docs.ainative.studio/docs/security/platform-security"
last-updated: "2026-10-13T21:42:35.442Z"
---

# Platform Security

Source: https://docs.ainative.studio/docs/security/platform-security

> Two-factor authentication, encryption, VPC inference, and security scanning

## Platform Security

AINative provides multiple layers of security: two-factor authentication for user accounts, encryption at rest for sensitive data, VPC-isolated inference for regulated industries, and automated security scanning for agent configurations.

## Two-Factor Authentication

SMS-based 2FA with backup codes, rate limiting, and account lockout protection. Codes are delivered via Twilio and stored as bcrypt hashes -- never in plain text.

### Security Properties

| Property | Value |
|----------|-------|
| Code length | 6 digits |
| Code expiry | 5 minutes (configurable) |
| Max attempts per code | 3 |
| Max codes per hour | 5 |
| Lockout threshold | 5 consecutive failed attempts |
| Lockout duration | 30 minutes |
| Backup codes | 10 alphanumeric codes (10 characters each) |
| Phone encryption | Fernet (AES-128-CBC) |
| Code storage | bcrypt hash |

Weak patterns such as `123456`, `111111`, and sequential digits are rejected during code generation.

### Enable 2FA

Validates the phone number via Twilio, encrypts it at rest, generates 10 backup codes, and activates 2FA on the account.

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/2fa/enable \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+14155551234"}'
```

**Response (200):**
```json
{
  "success": true,
  "message": "Two-factor authentication enabled successfully",
  "backup_codes": [
    "A3K9M2X7P1",
    "B8N4R6Y2Q5",
    "C1L7T3W9S4",
    "D5J2V8F6E3",
    "G9H1Z4U7K8",
    "H6P3X5M2R1",
    "J4Q8S1N7L6",
    "K2W5Y9D3T8",
    "L7E4F6A1V2",
    "M3U9G5B8C7"
  ]
}
```

Save the backup codes immediately. They are shown only once and stored as bcrypt hashes after this response.

### Generate and Send Code

Generates a 6-digit code and sends it to the user's registered phone via SMS.

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/2fa/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+14155551234"}'
```

**Response (200):**
```json
{
  "success": true,
  "message": "Verification code sent successfully",
  "expires_at": "2026-06-01T12:05:00",
  "message_sid": "SM1234567890abcdef"
}
```

Rate limited to 5 codes per hour per user. Previous unverified codes are invalidated when a new code is generated.

### Verify Code

Validates the 6-digit code against the stored bcrypt hash.

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/2fa/verify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code": "482917"}'
```

**Response (200) -- success:**
```json
{
  "success": true,
  "message": "Verification code is valid",
  "locked": false
}
```

**Response (200) -- failure:**
```json
{
  "success": false,
  "error": "Invalid verification code. 2 attempts remaining.",
  "locked": false
}
```

**Response (200) -- locked:**
```json
{
  "success": false,
  "error": "Account locked for 30 minutes due to too many failed attempts",
  "locked": true
}
```

### Verify Backup Code

Use a backup code when SMS is unavailable. Each code can only be used once.

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/2fa/backup \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code": "A3K9M2X7P1"}'
```

A successful verification consumes the backup code and resets the failed attempt counter.

### Regenerate Backup Codes

Generates a fresh set of 10 backup codes, replacing any existing codes.

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/2fa/backup/regenerate \
  -H "Authorization: Bearer $TOKEN"
```

### Get 2FA Status

Returns whether 2FA is enabled, a masked phone number, remaining backup codes, and lockout status.

```bash
curl https://api.ainative.studio/api/v1/auth/2fa/status \
  -H "Authorization: Bearer $TOKEN"
```

**Response (200):**
```json
{
  "enabled": true,
  "phone_number": "****1234",
  "verified_at": "2026-05-15T10:30:00",
  "backup_codes_remaining": 8,
  "is_locked": false,
  "lockout_until": null
}
```

### Disable 2FA

Disables 2FA, clears backup codes, and invalidates all pending verification codes. The caller must verify the user's current password before calling this endpoint.

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/2fa/disable \
  -H "Authorization: Bearer $TOKEN"
```

## Encryption Service

Sensitive data such as API tokens and credentials are encrypted at rest using Fernet symmetric encryption (AES-256) with PBKDF2 key derivation.

### How It Works

1. The `ENCRYPTION_SECRET` environment variable provides the master secret (minimum 32 characters).
2. A deterministic encryption key is derived using PBKDF2-HMAC-SHA256 with 100,000 iterations.
3. Data is encrypted with Fernet, producing a base64-encoded ciphertext safe for database storage.
4. Decryption reverses the process using the same derived key.

### Encrypted Data Types

| Data Type | Detection Method |
|-----------|-----------------|
| GitHub tokens | Checks for `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_` prefixes to distinguish plain text from encrypted |
| API keys | Generic encryption via `encrypt_api_key()` / `decrypt_api_key()` |

### Usage

```python
from app.services.encryption_service import encrypt_github_token, decrypt_github_token

## Encrypt before storing
encrypted = encrypt_github_token("ghp_abc123...")

## Decrypt when needed
plain = decrypt_github_token(encrypted)
```

The `is_github_token_encrypted()` helper detects whether a stored value is already encrypted, preventing double-encryption.

## VPC Inference

For regulated industries (finance, healthcare, government), AINative supports deploying the inference stack inside a customer's own cloud account via Helm chart. The platform manages licensing and usage metering.

**Base path:** `/api/v1/public/vpc`

### License Key Format

License keys follow the format `vpc_<base62(32 chars)>`. The raw key is shown once at issuance. Only a SHA-256 hash is stored in the database.

### POST /vpc/license/issue (Admin Only)

Issue a new VPC license key for a customer organization.

```bash
curl -X POST https://api.ainative.studio/api/v1/public/vpc/license/issue \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "org_id": "550e8400-e29b-41d4-a716-446655440000",
    "expires_at": "2027-06-01T00:00:00Z",
    "allowed_replicas": 5,
    "support_tier": "enterprise",
    "notes": "ACME Corp annual license"
  }'
```

**Response (200):**
```json
{
  "license_key": "vpc_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6",
  "license_id": "uuid",
  "org_id": "550e8400-e29b-41d4-a716-446655440000",
  "expires_at": "2027-06-01T00:00:00",
  "allowed_replicas": 5,
  "support_tier": "enterprise"
}
```

| Field | Description |
|-------|-------------|
| `allowed_replicas` | Maximum number of inference replicas (1--1000) |
| `support_tier` | One of `standard`, `enterprise`, `platinum` |

### POST /vpc/license/validate

Called by the customer-deployed Helm chart on startup and every 24 hours. No bearer token required -- the license key itself is the credential.

```bash
curl -X POST https://api.ainative.studio/api/v1/public/vpc/license/validate \
  -H "Content-Type: application/json" \
  -d '{
    "license_key": "vpc_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6",
    "deployment_id": "prod-us-east-1",
    "hostname": "inference-0.acme.internal",
    "version": "1.2.0"
  }'
```

**Response (200) -- valid:**
```json
{
  "valid": true,
  "org_id": "550e8400-e29b-41d4-a716-446655440000",
  "expires_at": "2027-06-01T00:00:00",
  "allowed_replicas": 5,
  "support_tier": "enterprise",
  "message": "OK"
}
```

**Response (200) -- invalid:**
```json
{
  "valid": false,
  "message": "License has expired"
}
```

Possible rejection reasons: `License key not found`, `License has been revoked`, `License is suspended -- contact support`, `License has expired`.

### POST /vpc/telemetry

Phone-home telemetry from customer VPC deployments, called hourly. Used for metered billing. No bearer token -- license key is the credential.

```bash
curl -X POST https://api.ainative.studio/api/v1/public/vpc/telemetry \
  -H "Content-Type: application/json" \
  -d '{
    "license_key": "vpc_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6",
    "deployment_id": "prod-us-east-1",
    "period_start": "2026-06-01T11:00:00Z",
    "period_end": "2026-06-01T12:00:00Z",
    "input_tokens": 150000,
    "output_tokens": 45000,
    "request_count": 1200,
    "model": "llama-3.1-70b",
    "hostname": "inference-0.acme.internal"
  }'
```

**Response (200):**
```json
{
  "received": true,
  "event_id": "uuid"
}
```

Returns `403` if the license key is invalid or expired.

### GET /vpc/license/\{license_id\} (Admin Only)

Fetch license details and aggregated telemetry event count.

```bash
curl https://api.ainative.studio/api/v1/public/vpc/license/LICENSE_UUID \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

**Response (200):**
```json
{
  "license_id": "uuid",
  "org_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "active",
  "expires_at": "2027-06-01T00:00:00",
  "allowed_replicas": 5,
  "support_tier": "enterprise",
  "issued_at": "2026-06-01T00:00:00",
  "notes": "ACME Corp annual license",
  "last_validated_at": "2026-06-01T12:00:00",
  "total_events": 8472
}
```

## Security Scanning

Automated scanning of Claude Code settings, MCP server configurations, and skill files for security vulnerabilities. Inspired by the AgentShield pattern.

**Base path:** `/api/v1/public/security/scan`

### POST /scan

Submit a file for security analysis. Returns structured findings with severity levels and a 0--100 security score.

```bash
curl -X POST https://api.ainative.studio/api/v1/public/security/scan \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "settings",
    "content": "{\"permissions\": {\"deny\": []}}"
  }'
```

**Scan Types:**

| Type | What It Checks |
|------|----------------|
| `settings` | Claude Code `settings.json` -- deny rules, hook commands |
| `mcp_config` | MCP server config -- unknown servers, hardcoded credentials |
| `skill` | Skill/prompt files -- injection patterns, competitor references |

**Response (200):**
```json
{
  "ok": true,
  "scan_id": "uuid",
  "type": "settings",
  "threats": [
    {
      "severity": "critical",
      "category": "missing_deny_rule",
      "description": "SSH key access not blocked in deny rules",
      "recommendation": "Add Read(~/.ssh/**) to deny rules",
      "line": null
    },
    {
      "severity": "critical",
      "category": "missing_deny_rule",
      "description": "AWS credential access not blocked",
      "recommendation": "Add Read(~/.aws/**) to deny rules",
      "line": null
    }
  ],
  "score": 35,
  "passed": false,
  "summary": {
    "critical": 2,
    "high": 1,
    "medium": 0,
    "low": 0
  },
  "scanned_at": "2026-06-01T12:00:00Z"
}
```

### Scoring

The security score starts at 100 and is reduced by severity-weighted penalties:

| Severity | Penalty |
|----------|---------|
| Critical | -25 points |
| High | -15 points |
| Medium | -5 points |
| Low | -2 points |

A score of 70 or above is considered **passed**. Below 70 is **failed**.

### Settings Scan Checks

The settings scanner validates:

- **Deny rules** -- checks that `~/.ssh/**`, `~/.aws/**`, and `curl * | bash` patterns are blocked
- **Hook commands** -- flags hooks that make outbound network calls via `curl`, `wget`, `nc`, or `netcat`

Deny rules are normalized (whitespace-collapsed) and support both string and object formats.

### MCP Config Scan Checks

The MCP config scanner validates:

- **Unknown servers** -- flags any MCP server not in the vetted allowlist (`ainative-zerodb-memory`, `sequential-thinking`, `context7`, `filesystem`, `google-analytics`, `google-ads`, `railway`, `zerodb-memory`, `ainative-gtm`)
- **Credential exposure** -- detects hardcoded values in `env` keys containing `secret`, `key`, `token`, or `password` (values starting with `$` are treated as safe env var references)

### Skill Scan Checks

The skill scanner detects:

- **Injection patterns** -- instruction overrides, unicode homoglyph attacks, XML role injection, Llama-style injection markers
- **Competitor references** -- mentions of Supabase, NeonDB, PlanetScale, or DevFleet

:::caution Not currently enforced
This endpoint is not rate limited today. Kong (previously referenced here) has been fully retired in favor of APISIX, and no rate-limiting plugin for this route exists in the current APISIX config or in application code. Treat any rate-limiting behavior as unconfirmed until it is actually configured.
:::

## Environment Variables

| Variable | Service | Purpose |
|----------|---------|---------|
| `TWO_FA_ENCRYPTION_KEY` | 2FA | Fernet key for phone number encryption |
| `TWO_FA_CODE_EXPIRY_MINUTES` | 2FA | Code lifetime (default: 5) |
| `TWO_FA_MAX_ATTEMPTS` | 2FA | Max verification attempts per code (default: 3) |
| `TWO_FA_LOCKOUT_MINUTES` | 2FA | Lockout duration after failed attempts (default: 30) |
| `ENCRYPTION_SECRET` | Encryption | Master secret for PBKDF2 key derivation (32+ chars) |
| `STRIPE_SECRET_KEY` | Payments | Stripe API secret key |
| `STRIPE_WEBHOOK_SECRET` | Payments | Webhook signature verification |
