API Security Evidence

Evidence of authentication, rate limiting, input validation, CORS policies, and security headers across API services

Public

Status: mixed. This evidence reflects implemented code and deployed infrastructure across both Maelstrom AI platforms. Provii is not yet serving end-user production traffic, so its production operational metrics and audit history are not yet available; downpipes is in production, and Maelstrom’s own Cloudflare estate is backed up by Maelstrom’s own production instance of downpipes (we dogfood our own product).

API Security Evidence Collection

Control Coverage: UC-044 through UC-051 Date Generated: 2026-02-14 Status: Complete Repositories Analysed: 4 Provii backend services (provii-verifier, provii-issuer, provii-management, provii-credit-management). downpipes API surface (engine, console, control-plane) covered in line at the end of each control.


Executive Summary

This document provides evidence for API security controls across the Maelstrom AI backend infrastructure, which serves both in-scope platforms — Provii (privacy-preserving age verification) and downpipes (no-custody backup and disaster recovery for the Cloudflare data layer). These API security controls are cross-cutting: the same defence-in-depth posture — TLS enforcement, HMAC authentication, RBAC authorisation, security headers, rate limiting, and input validation — governs the Provii services and the downpipes API surface.

The operating model differs by platform and shapes where each control is enforced. The Provii verification and issuance services are operated by Maelstrom. For downpipes, the engine (routeless, in-tenant) and console run in the customer’s own Cloudflare account, under the customer’s keys (customer-operated; complementary user-entity controls), while Maelstrom operates only the vendor-side control-plane and the signature-pinned update channel. The downpipes architecture is no-custody: Maelstrom holds no customer keys, data or Cloudflare tokens, the engine is routeless and keeps no standing inbound path (“runs dark”), and the control-plane is fail-open (it never gates backup or restore). Where a control’s posture differs by platform, the difference is called out in the relevant section.

Key Findings:

  • TLS enforcement via Cloudflare across all services (UC-044)
  • Multi-layered authentication: HMAC-SHA256, JWT sessions, API keys (UC-045)
  • RBAC and BOLA protection (UC-046)
  • Security headers on all services (13 headers on provii-verifier)
  • Rate limiting via per-service KV counters and per-endpoint middleware
  • Input validation using Zod schemas (TypeScript) and strict newtype wrappers (Rust)
  • Audit logging to KV with 90-day retention; critical security event logs are retained for up to 365 days
  • For downpipes: a no-custody, runs-dark posture means the engine is routeless with no public route and no standing inbound path; the vendor control-plane is fail-open; updates are pulled from a signature-pinned channel (never pushed); and any vendor diagnostics are consent-gated (owner-minted, time-boxed, scoped, immediately-revocable read-only pull credentials)

Table of Contents

  1. UC-044: Encryption in Transit (TLS)
  2. UC-045: Authentication Mechanisms
  3. UC-046: Authorisation Controls
  4. UC-047: Incident Response
  5. UC-048: Vulnerability Management
  6. UC-049: Security Monitoring
  7. UC-050: Penetration Testing
  8. UC-051: Security Awareness Training
  9. API Security Best Practices
  10. downpipes API Surface
  11. Evidence Artefacts

UC-044: Encryption in Transit (TLS)

Implementation

Cloudflare Universal SSL/TLS: All Maelstrom AI backend services are deployed on Cloudflare Workers, which provides:

  • TLS 1.2+ enforcement (TLS 1.3 preferred)
  • Automatic certificate management and renewal
  • HTTP Strict Transport Security (HSTS)
  • Automatic HTTP to HTTPS redirects

Evidence

1. HSTS Headers (All Services)

provii-verifier (src/security/headers.rs):

headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")?;

provii-management (src/middleware/security-headers.ts:48):

// Always enabled (Cloudflare Workers run on HTTPS by default)
c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

provii-credit-management (src/utils/security-headers.ts:27):

'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',

2. TLS Configuration Evidence

Deployment Platform:

  • All services: wrangler.toml -> Cloudflare Workers deployment
  • Cloudflare enforces TLS 1.2+ by default
  • No plaintext HTTP endpoints exposed to public internet

Local Development:

  • Development environments use http://localhost only
  • Production/staging enforce HTTPS exclusively

3. Certificate Management

Cloudflare Automatic Certificate Management:

  • Universal SSL certificates auto-issued for all *.provii.app and *.zerokp.id domains
  • Automatic renewal (60-90 days before expiration)
  • Certificate transparency logging enabled
  • OCSP stapling for revocation checking

Evidence Location:

  • Cloudflare Dashboard -> SSL/TLS -> Edge Certificates
  • HSTS preload list submission: hstspreload.org (pending)

4. downpipes (TLS in transit, customer-controlled at rest)

All downpipes API traffic is TLS-terminated by Cloudflare in the same way:

  • The vendor-operated control-plane and the console’s Worker proxy run on Cloudflare Workers (HTTPS by default; TLS 1.2+ enforced, TLS 1.3 preferred).
  • The static, signature-pinned update channel (updates.downpipes.io, an R2 bucket) is served over HTTPS; the engine pulls updates and verifies each update’s signature against a pinned public key before applying (transport security plus content authenticity).
  • Because downpipes is no-custody, confidentiality of customer data does not rely on transport alone: archives are sealed with post-quantum hybrid encryption under customer-held keys before they ever leave the customer’s account (see UC-049/best practices and the cryptography policy).

UC-045: Authentication Mechanisms

Overview

Provii backend services implement multiple authentication mechanisms:

  1. HMAC-SHA256 per-request signatures (provii-verifier, provii-issuer, provii-management)
  2. JWT session tokens (provii-management via Logto)
  3. API key authentication (provii-issuer, provii-credit-management)

downpipes authentication is covered separately in downpipes API Surface: operator access to the in-tenant console is authenticated by the customer’s own identity provider inside the customer’s tenant; the vendor-operated control-plane authenticates licence tokens only; and vendor diagnostics, when needed, use owner-minted, time-boxed, scoped, immediately-revocable read-only pull credentials (no standing vendor access).

1. HMAC-SHA256 Authentication

provii-verifier

Implementation: provii-verifier/src/security/auth.rs

Canonical Message Format:

// From src/routes/challenge.rs lines 245-273
fn create_canonical_message_for_challenge(
    method: &str,
    path: &str,
    timestamp: u64,
    body: &CreateChallengeRequest,
) -> String {
    let payload = json!({
        "code_challenge": body.code_challenge.to_string(),
        "method": body.method,
        "requested_cutoff_days": body.requested_cutoff_days.map(|c| c.get()),
        "verifying_key_id": body.verifying_key_id.map(|v| v.get()),
        "expires_in": body.expires_in.get(),
        "proof_direction": body.proof_direction,
    });

    format!("{}:{}:{}:{}", timestamp, method, path, payload.to_string())
}

Security Properties:

  • 256-bit HMAC secret (CSPRNG-generated)
  • Replay protection via timestamp validation (300-second window)
  • Nonce-based duplicate request detection
  • Constant-time signature verification (ct_eq in auth.rs)
  • Request integrity protection (method + path + body)

Documentation: provii-verifier/docs/security/AUTHENTICATION_PATHWAYS.md

provii-issuer

Implementation: provii-issuer/src/security.rs

Request Signature:

// Lines 43-56
pub struct RequestSignature {
    pub method: String,
    pub path: String,
    pub query_params: Vec<(String, String)>,
    pub content_type: String,
    pub body_hash: String,
    pub timestamp: u64,
}

impl RequestSignature {
    pub fn to_canonical_string(&self) -> String {
        let mut sorted_params = self.query_params.clone();
        sorted_params.sort_by(|a, b| a.0.cmp(&b.0));

        let query_string = sorted_params
            .iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect::<Vec<_>>()
            .join("&");

        format!(
            "{}\n{}\n{}\n{}\n{}\n{}",
            self.method, self.path, query_string, self.content_type, self.body_hash, self.timestamp
        )
    }
}

Verification:

// Lines 89-98
pub fn verify(&self, secret: &[u8], signature: &[u8]) -> Result<bool> {
    type HmacSha256 = Hmac<Sha256>;
    let mut mac = HmacSha256::new_from_slice(secret)
        .map_err(|e| ApiError::CryptoError(format!("Invalid HMAC key: {}", e)))?;

    mac.update(self.to_canonical_string().as_bytes());

    // Constant-time verification
    Ok(mac.verify_slice(signature).is_ok())
}

provii-management

Implementation: provii-management/src/middleware/auth.ts

Canonical Message:

// Lines 131-133 of auth.ts
const method = c.req.method;
const path = c.req.path + (c.req.url.includes('?') ? '?' + new URL(c.req.url).search.slice(1) : '');
const canonicalMessage = createCanonicalMessage(timestamp, method, path, bodySha256);

// from utils/hmac.ts:
export function createCanonicalMessage(timestamp: number, method: string, path: string, bodyHash: string): string {
  return `${timestamp}:${method}:${path}:${bodyHash}`;
}

HMAC Verification:

// auth.ts line 136-162
const secret = await c.env.MANAGEMENT_HMAC_KEY.get(); // Secrets Store binding (async)

const isValidSignature = await verifyHMAC(secret, signature, canonicalMessage);

if (!isValidSignature) {
  console.warn('Authentication failed: Invalid HMAC signature', {
    sessionId,
    path,
    method,
    timestamp,
    canonicalMessage,
    receivedSignature: signature.substring(0, 30) + '...',
  });
  throw new HTTPException(401, { message: 'Invalid request signature' });
}

2. JWT Session Tokens

provii-management (Durable Object SessionManager)

Session Retrieval and Validation:

// Lines 168-177 from auth.ts
const session = await getAdminSessionFromDO(c.env.ADMIN_PORTAL_SESSION_MANAGER, sessionId);

if (!session) {
  throw new HTTPException(401, {
    message: 'Invalid or expired session',
    cause: AuthErrorType.SESSION_NOT_FOUND,
  });
}

// Validate session expiration
if (session.expiresAt < now) {
  throw new HTTPException(401, { message: 'Session has expired' });
}

Session Binding Validation:

// Lines 210-214
// CRITICAL-04 FIX: Validate session binding
await validateSessionBinding(c);

// HIGH-05 FIX: Validate session expiration
await validateSessionExpiration(c);

3. API Key Authentication

provii-issuer

Implementation: provii-issuer/src/security.rs:119-260

pub async fn verify_api_key(env: &Env, api_key: &str) -> Result<ClientRegistration> {
    let kv = env
        .kv("ISSUER_CLIENTS")
        .map_err(|e| ApiError::StorageError(format!("Failed to get KV namespace: {}", e)))?;

    let list_result = kv
        .list()
        .execute()
        .await
        .map_err(|e| ApiError::StorageError(format!("Failed to list clients: {}", e)))?;

    // Try each client (with Argon2id hash verification)
    for (idx, key_info) in list_result.keys.iter().enumerate() {
        if let Ok(Some(data)) = kv.get(&key_info.name).text().await {
            match serde_json::from_str::<ClientRegistration>(&data) {
                Ok(client) => {
                    if !client.active {
                        continue;
                    }
                    // Verify Argon2id hash (constant-time via Argon2 library)
                }
            }
        }
    }

    Err(ApiError::Unauthorized("Invalid API key".to_string()))
}

Security Properties:

  • API keys hashed with Argon2id
  • Constant-time comparison (built into Argon2id verification)
  • Active/inactive status checking
  • KV-based storage with encryption at rest (hashes encrypted before storage)

UC-046: Authorisation Controls

RBAC Implementation

provii-management

Role Definitions:

// From types/index.ts line 211
export type AdminRole = 'super_admin' | 'admin' | 'viewer';

// From types/index.ts lines 365-404
export interface AdminSession {
  sessionId: string;
  userId: string;
  email: string;
  role: AdminRole;
  createdAt: number;
  expiresAt: number;
  last_activity: number;
  ip_hash: string;
  user_agent_hash: string;
  allowed_environments?: string[];
  organization_id?: string;
}

Role Checking:

// From middleware/auth.ts lines 328-344
function isValidSession(data: unknown): data is AdminSession {
  if (!data || typeof data !== 'object') {
    return false;
  }

  const session = data as Partial<AdminSession>;

  return (
    typeof session.sessionId === 'string' &&
    typeof session.userId === 'string' &&
    typeof session.email === 'string' &&
    typeof session.role === 'string' &&
    ['viewer', 'admin', 'super_admin'].includes(session.role) &&
    typeof session.createdAt === 'number' &&
    typeof session.expiresAt === 'number'
  );
}

BOLA Protection (Broken Object Level Authorisation)

provii-verifier

Implementation: provii-verifier/src/routes/redeem.rs:168-257

// SECURITY: Mandatory ownership verification (OWASP API1:2023, CWE-639)
match &cached.client_id {
    Some(owner_id) if owner_id == &client_id => {
        console_log!(
            "[SECURITY] redeem_challenge: Ownership verified for challenge {} by client {}",
            redact_challenge_id(&sid.to_string()),
            client_id
        );
    }
    Some(owner_id) => {
        console_log!(
            "[SECURITY] redeem_challenge: Ownership verification failed - challenge {} owned by '{}', accessed by '{}'",
            redact_challenge_id(&sid.to_string()),
            owner_id,
            client_id
        );
        return ApiError::Forbidden("Access denied: you do not own this challenge").to_response();
    }
    None => {
        console_log!(
            "[SECURITY] redeem_challenge: Ownership verification failed - challenge {} has no owner",
            redact_challenge_id(&sid.to_string())
        );
        return ApiError::Forbidden("Access denied: challenge ownership not established").to_response();
    }
}

Documentation: provii-verifier/docs/security/AUTHENTICATION_PATHWAYS.md (Lines 449-558)

CVSS Score: 7.5 (High) - Mitigated

Migration Status:

  • Phase 1 (Complete): All new challenges include client_id
  • Phase 2 (In Progress): Monitor legacy challenge usage
  • Phase 3 (Month 4-5): 60-day grace period with enforcement date
  • Phase 4 (Month 10): Strict enforcement, reject all legacy challenges

Principle of Least Privilege

provii-verifier - Asymmetric Security Design

Public Endpoint (GET /v1/challenge/{id}):

  • No authentication required (UUID serves as capability credential)
  • Returns minimal data: challenge_id, short_code, rp_challenge, cutoff_days, verifying_key_id, submit_secret, expires_at, proof_direction, status_url, verify_url
  • submit_secret is a single-use 32-byte bearer token
  • Sensitive data (origin, client_id, PKCE) requires authentication via separate poll endpoint

Authenticated Endpoints:

  • POST /v1/redeem. Requires PKCE code_verifier + submit_secret
  • POST /v1/challenge. Requires HMAC authentication + Client ID extraction
  • GET /v1/challenge/{id}/poll. Requires HMAC authentication + ownership verification

Rationale: Users need to retrieve challenges by short code (accessibility), but sensitive operations require strong authentication and authorisation.


UC-047: Incident Response

Audit Logging

provii-management

Audit Middleware: provii-management/src/middleware/audit.ts

Log Format:

// From types/index.ts lines 219-265
export interface AuditEvent {
  eventId: string;
  timestamp: number;
  action: string;
  category: string;
  adminUser: {
    userId: string;
    email: string;
    role: string;
  };
  target?: {
    type: string;
    id: string;
  };
  request: {
    ip: string;
    userAgent: string;
    requestId: string;
    sessionId?: string;
    correlationId?: string;
  };
  success: boolean;
  error?: string;
  metadata?: Record<string, any>;
  statusCode?: number;
  durationMs?: number;
}

All API Calls Logged:

// Applied globally (src/index.ts:149)
app.use('*', auditMiddleware);

provii-verifier

Security Logging Examples:

BOLA Attempt Detection:

// redeem.rs lines 228-233
console_log!(
    "[SECURITY] redeem_challenge: Ownership verification failed - challenge {} owned by '{}', accessed by '{}'",
    redact_challenge_id(&sid.to_string()),
    owner_id,
    client_id
);

Authentication Failures:

// auth.rs - HMAC mismatch logging with CWE-778 reference
console_log!("[Auth] HMAC mismatch for client_id={}", client.client_id);
// Structured logging with failure_type: "hmac_signature_mismatch"

Incident Response Procedures

Documentation: src/content/trust/security/incident-response.mdx

Incident Classification:

  • Critical (P0): Authentication bypass, data breach, service outage
  • High (P1): BOLA attempts, rate limit abuse, suspicious traffic patterns
  • Medium (P2): Failed authentication attempts, input validation errors
  • Low (P3): Configuration warnings, certificate expiry notices

Incident Tracking:

  • All audit events logged to MANAGEMENT_AUDIT_LOGS KV namespace (90-day retention; critical security event logs are retained for up to 365 days)
  • Slack webhook integration exists for admin-portal alerts
  • Post-incident review procedures defined in incident-response.mdx

Because downpipes runs dark and Maelstrom holds no customer data, incident detection is owner-facing and in-tenant rather than vendor-collected:

  • The engine and console emit a scored security posture with regression alerts and operator-paging notification rules; hourly canary integrity flights confirm archives remain well-formed and decryptable.
  • Where Maelstrom needs to assist with an incident, the owner mints a time-boxed, scoped, immediately-revocable read-only pull credential alongside a redaction-safe signed support bundle. There is no standing vendor access and no standing inbound path into the customer tenant.
  • The vendor-operated control-plane is fail-open: a control-plane incident never gates the customer’s backup or restore.

UC-048: Vulnerability Management

Dependency Scanning

Automated Scanning:

  • npm audit for TypeScript services (provii-management, provii-credit-management)
  • cargo audit for Rust services (provii-verifier, provii-issuer)
  • Dependabot enabled on all GitHub repositories
  • Semgrep for SAST analysis
  • Trivy for vulnerability scanning
  • Gitleaks for secret detection

Evidence:

# Security workflows vary by repo type:
# Rust repos: .github/workflows/security-audit.yml
# TypeScript repos: .github/workflows/security.yml
# All use SHA-pinned actions (e.g., actions/checkout@v6.0.2)

downpipes supply chain (update channel + offline Update Signer): The downpipes engine, console, control-plane and offline reader are built on the same CI/CD and dependency-scanning model. Because the engine and console run in customer tenants, downpipes adds a supply-chain control at the point of update distribution:

  • Engine and console updates are published to a static, signature-pinned R2 bucket (updates.downpipes.io); the engine verifies each update’s signature against a pinned public key before applying.
  • Updates are pulled, never pushed; there is no standing inbound path into the customer tenant. Safe-apply is canary-gated with automatic rollback, and there is no phone-home.
  • Releases are signed with the downpipes Update Signer, held offline by the operator and never deployed to any Worker. The offline downpipe reader is built from a frozen public format spec, so archive recovery never depends on a vendor-supplied binary.

Penetration Testing

Planned Testing:

  • Internal Testing: Quarterly security assessments
  • External Pentest: Annual third-party penetration test
  • Responsible Disclosure: security@maelstrom.au

Vulnerability Disclosure

Contact: security@maelstrom.au

Disclosure Policy:

  • Report submitted -> 48-hour acknowledgment
  • Initial assessment -> 5 business days
  • Fix timeline -> 30 days for critical, 90 days for high/medium
  • Public disclosure -> Coordinated with reporter

UC-049: Security Monitoring

Rate Limiting

All rate limiting uses a simple KV counter pattern (non-atomic increment with TTL, fail-open on KV errors). Tier-based per-customer limits are loaded from RATE_LIMIT_CONFIG KV and cached per-isolate for 60 seconds. The shared-rate-limit library and its Durable Object have been retired and archived.

Architecture:

┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Cloudflare Edge (WAF rate limiting, 5,000/10min/IP)│
├─────────────────────────────────────────────────────────────┤
│ Layer 2: Per-service KV counters (hourly, tier-based)       │
│   - Per-customer quota (authenticated, from tier config)    │
│   - Per-IP limit (short code enumeration, provii-verifier)     │
│   - Per-issuer limit (blind issuance, provii-issuer)           │
│   - In-memory auth limiter (admin-portal only)              │
└─────────────────────────────────────────────────────────────┘

Per-Service Limits:

ServiceLimit TypeDefaultIdentifierConfigurable Via
provii-verifierPer-customer hourly quotaDEFAULT_QUOTA_PER_HOUR env varAuthenticated client IDKV tier config
provii-verifierShort code enumerationSHORT_CODE_LIMIT_PER_HOUR (60/hr)CF-Connecting-IPwrangler.toml env var
provii-issuerPer-customer hourly quotaDEFAULT_QUOTA_PER_HOUR env varAuthenticated client IDKV tier config
provii-issuerBlind issuance per-issuerBLIND_ISSUANCE_LIMIT_PER_HOUR (1,000/hr)Issuer IDwrangler.toml env var
provii-verifier (hosted mode)Per-customer hourly quotaDEFAULT_QUOTA_PER_HOUR env varPublic key (pk_)KV tier config
admin-portalAuth brute-force5/min per IPCF-Connecting-IPIn-memory Map
provii-managementNone (internal, behind admin-portal auth)...
provii-credit-managementNone (internal, service-to-service only)...

HTTP 429 Response Format:

  • Retry-After header (seconds remaining in current hour)
  • JSON body: { "error": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded", "retry_after": <seconds> }

Nonce Replay Prevention: NonceDO (src/durable_objects/nonce_do.rs) handles nonce replay detection separately from rate limiting. Nonces expire after 5 minutes (300 seconds).

downpipes (routeless engine; minimal vendor surface): The downpipes attack surface for rate limiting is deliberately small. The engine is routeless — it has no public route and no standing inbound path — so it presents no inbound endpoint to rate-limit. The only vendor-facing surfaces are the control-plane (which mints licence tokens and receives a content-free advisory beacon) and the static update channel (read-only R2, pull-only). Both sit behind Cloudflare edge protections, and the control-plane is fail-open so that rate-limiting or availability behaviour on the vendor side never gates a customer’s backup or restore.

Alerting

Implemented:

  • Slack webhook integration for admin-portal events
  • Audit log storage in MANAGEMENT_AUDIT_LOGS KV (90-day retention; critical security event logs are retained for up to 365 days)
  • Cloudflare Workers Logs (shipped to Grafana Loki) for real time metrics
  • downpipes self-monitoring (in-tenant, runs dark): scored security posture with regression alerts, hourly canary integrity flights, and operator-paging notification rules

Planned (not yet implemented):

  • PagerDuty integration for critical alerts
  • Email daily digest
  • Bespoke Grafana dashboards for rate limit monitoring
  • ML-based anomaly detection

UC-050: Penetration Testing

Testing Schedule

Frequency:

  • Internal security testing: Quarterly
  • External penetration testing: Annual
  • Responsible disclosure: Continuous (security@maelstrom.au)

Test Scope

In-Scope:

  • All Provii API endpoints (provii-verifier, provii-issuer, provii-management, provii-credit-management)
  • Authentication mechanisms (HMAC, JWT, API keys)
  • Authorisation controls (RBAC, BOLA)
  • Rate limiting effectiveness
  • Input validation
  • Session management
  • Cryptographic implementations
  • downpipes vendor-side surfaces: the control-plane (licence-token minting, advisory beacon) and the static, signature-pinned update channel (signature verification, pull-only safe-apply)

Out-of-Scope:

  • Physical security
  • Social engineering
  • Denial of service attacks (without pre-approval)
  • Third-party services (Cloudflare, Logto, Stripe)
  • downpipes in-tenant components (the engine and console) as deployed inside a customer’s own Cloudflare account — these are customer-operated (complementary user-entity controls) and sit outside Maelstrom’s operational boundary; Maelstrom holds no customer keys, data or Cloudflare tokens

Methodology

Standards:

  • OWASP API Security Top 10 (2023)
  • OWASP ASVS 5.0.0 (Level 3)
  • NIST SP 800-115 (Technical Guide to Information Security Testing)

Phases:

  1. Reconnaissance: Identify attack surface, enumerate endpoints
  2. Vulnerability Discovery: Automated scanning + manual testing
  3. Exploitation: Attempt to exploit discovered vulnerabilities
  4. Privilege Escalation: Test lateral movement and privilege escalation
  5. Reporting: Document findings with CVSS scores and remediation guidance

Previous Testing Results

provii-verifier ASVS 5.0.0 Hardening (2025-11-07):

Critical fixes implemented as part of ASVS alignment:

  1. V3.5.8. Sec-Fetch header validation (fail-closed enforcement)
  2. V14.2.5. Cache headers (cache deception prevention)
  3. V13.3.4. Secret expiry enforcement (fail-fast logic)
  4. V3.4.7. CSP endpoint rate limiting (DoS prevention)
  5. V11.7.2. Debug redaction (secret leakage prevention)

UC-051: Security Awareness Training

Training Programme

Target Audience:

  • All ISMS roles (sole operator. handles development, DevOps, security, and product)
  • Contractors with access to backend services (when engaged)

Training Modules:

  1. Secure Coding Practices:
  • Input validation and sanitisation
  • Output encoding
  • XSS prevention
  • CSRF protection
  • Authentication and session management
  1. API Security:
  • OWASP API Security Top 10
  • HMAC signature generation and validation
  • Rate limiting implementation
  • BOLA/IDOR prevention
  • Secure error handling
  • No-custody / runs-dark API design for downpipes (routeless engine, fail-open control-plane, signature-pinned pull-only updates, consent-gated diagnostics)
  1. Cryptography:
  • Key management best practices
  • Encryption at rest and in transit
  • Hash function selection (Argon2id for passwords)
  • Random number generation (CSPRNG)
  1. Incident Response:
  • Security incident classification
  • Reporting procedures
  • Escalation paths
  • Post-incident review process

Training Delivery

Format:

  • Quarterly training sessions (1-2 hours)
  • Annual security awareness day
  • On-demand video modules

Documentation:

  • src/content/trust/security/security-awareness.md
  • src/content/trust/security/roles-responsibilities.md

Tracking:

  • Training completion tracked in HR system
  • Annual refresher required for all team members
  • Security champions programme for cross-functional teams

API Security Best Practices

Security Headers

All backend services implement security headers:

provii-verifier

Implementation: provii-verifier/src/security/headers.rs

Default Headers Applied (13 total):

  1. Content-Security-Policy:

    default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none';
    font-src 'none'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';
    object-src 'none'; form-action 'none'; report-uri /v1/csp-report; report-to csp-endpoint
  2. Report-To: CSP reporting endpoint configuration

  3. Cross origin-Opener-Policy: same-origin

  4. Cross origin-Embedder-Policy: require-corp

  5. Cross origin-Resource-Policy: same-origin

  6. X-Frame-Options: DENY

  7. X-Content-Type-Options: nosniff

  8. X-XSS-Protection: 1; mode=block

  9. Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

  10. Referrer-Policy: no-referrer

  11. Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()

  12. Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0

  13. X-Permitted-Cross-Domain-Policies: none

Additional headers (Pragma: no-cache, Expires: 0) are applied to API-specific endpoints via api_security_headers().

provii-management

Implementation: provii-management/src/middleware/security-headers.ts (lines 35-89)

Headers Applied:

c.header('X-Content-Type-Options', 'nosniff');
c.header('X-Frame-Options', 'DENY');
c.header('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'");
c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
c.header('Referrer-Policy', 'no-referrer');
c.header('Permissions-Policy',
  'accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), ' +
  'cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), ' +
  // ... 27 directives total (all denied)
);

provii-credit-management

Implementation: provii-credit-management/src/utils/security-headers.ts (lines 23-119)

Dynamic Headers Based on Path Sensitivity:

export function isSensitivePath(path: string): boolean {
  const sensitivePaths = [
    '/v1/credits/balance',
    '/v1/credits/consume',
    '/v1/admin/entities',
    '/v1/admin/transactions',
  ];
  return sensitivePaths.some(sensitive => path.startsWith(sensitive));
}

// If sensitive:
headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, private';
headers['Pragma'] = 'no-cache';
headers['Expires'] = '0';

Input Validation

provii-management

Implementation: provii-management/src/utils/validation.ts

Zod-Based Validation:

export function validateRequest<T>(schema: z.ZodSchema<T>) {
  return async (body: unknown): Promise<
    { success: true; data: T } |
    { success: false; error: string; details: any[] }
  > => {
    try {
      const data = await schema.parseAsync(body);
      return { success: true, data };
    } catch (error) {
      if (error instanceof z.ZodError) {
        const details = error.issues.map((e: z.ZodIssue) => ({
          path: e.path.join('.'),
          message: e.message,
        }));
        return { success: false, error: details[0]?.message ?? 'Validation failed', details };
      }
      return { success: false, error: 'Invalid request body', details: [] };
    }
  };
}

Common Validators:

export const ShortString = z.string().min(1).max(256);
export const MediumString = z.string().min(1).max(1000);
export const LongString = z.string().min(1).max(10000);
export const Email = z.string().email().max(256);
export const PositiveInteger = z.number().int().positive();
export const NonNegativeInteger = z.number().int().min(0);
export const UUID = z.string().uuid();
export const SecureURL = z.string()
  .url()
  .max(500)
  .refine(
    (url) => url.startsWith('https://') || url.startsWith('http://localhost'),
    { message: 'URL must use HTTPS (or HTTP for localhost)' }
  );

provii-verifier

Implementation: provii-verifier/src/types/strict.rs

Strict Newtype Wrappers for Input Validation:

  • B64Url32: Validates base64url-encoded 32-byte values
  • B64Url192: Validates base64url-encoded 192-byte values
  • ShortCode: Validates 12-digit numeric short codes
  • CutoffDays: Validates age in days (-25,000 to 54,750 range, i32)
  • ExpiresIn: Validates time-to-live (30-300 seconds, u64)
  • PkceMethod: Enum accepting only “S256”

CreateChallengeRequest:

#[derive(Debug, Clone, Deserialize)]
pub struct CreateChallengeRequest {
    pub code_challenge: B64Url32,
    pub method: PkceMethod,          // Enum, not free-form String
    pub requested_cutoff_days: Option<CutoffDays>,
    pub verifying_key_id: Option<VkId>,
    pub expires_in: ExpiresIn,
    pub proof_direction: String,
    pub authorizer: Authorizer,
}

CSRF Protection

provii-management Implementation:

Single-Use Token Generation:

// csrf.ts lines 31-49
export async function generateCSRFToken(env: Env, sessionId: string): Promise<string> {
  const token = crypto.randomUUID();
  const key = `${CSRF_TOKEN_PREFIX}${sessionId}:${token}`;

  await env.ADMIN_SESSIONS.put(
    key,
    JSON.stringify({
      session_id: sessionId,
      created_at: Date.now(),
      expires_at: Date.now() + (CSRF_TOKEN_TTL * 1000),
    }),
    { expirationTtl: CSRF_TOKEN_TTL }  // 3600 seconds (1 hour)
  );

  return token;
}

Token Validation and Deletion:

// csrf.ts lines 65-104
// Validates session ownership, checks expiration, deletes after use (single-use pattern)

Middleware Application:

// index.ts line 146
app.use('*', csrfProtection());

CSRF Exemption for HMAC-Authenticated Requests: CSRF checks are skipped for unauthenticated requests (no session), as HMAC authentication provides inherent CSRF protection.

CORS Configuration

provii-management

Strict Origin Validation:

// index.ts lines 112-137
app.use('*', cors({
  origin: (origin) => {
    if (!origin) {
      return '*'; // Service Binding calls (worker-to-worker)
    }

    const allowedOrigins = [
      'https://admin.zerokp.id',
      'https://admin-staging.zerokp.id',
      'http://localhost:5173',
    ];

    if (!allowedOrigins.includes(origin)) {
      throw new HTTPException(403, { message: 'Origin not allowed' });
    }

    return origin;
  },
  credentials: true,
  allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization', 'X-Admin-Session-ID', 'X-Timestamp', 'X-Signature'],
  maxAge: 86400, // 24 hours
}));

provii-verifier

Origin Policy Enforcement:

  • Origin policies stored in KV namespace (VERIFIER_KV_CONFIG with origin:* key pattern)
  • Only approved origins can create challenges
  • HMAC authentication required for challenge creation and redemption endpoints
  • Public GET endpoints (challenge lookup, JWKS) do not require HMAC

downpipes API Surface

The sections above evidence the Provii backend services. downpipes — Maelstrom’s no-custody backup and disaster-recovery platform for the Cloudflare data layer — has a deliberately minimal and differently-shaped API surface. The same API security controls apply, but the operating model places most of the surface inside the customer’s own Cloudflare account.

Components and Trust Boundary

ComponentOperated byAPI posture
engineCustomer (in customer’s own Cloudflare account)Routeless Cloudflare Worker — no public route, no standing inbound path (“runs dark”). Reads sources, seals archives with post-quantum hybrid encryption, 3-2-1 fan-out to 2+ customer-controlled destinations.
consoleCustomer (in customer’s own account)Operator SPA + Worker proxy for config, restore approvals, posture and reports. Operator access authenticated by the customer’s own identity provider.
control-planeMaelstrom (vendor-operated)Mints licence tokens and receives a content-free advisory beacon. Holds no customer keys, data or Cloudflare tokens. Fail-open — never gates backup or restore.
update channelMaelstrom (vendor-operated)Static, signature-pinned R2 bucket (updates.downpipes.io). Read-only; the engine pulls verified updates (never pushed); no phone-home.
downpipe (offline reader)Customer (offline)MIT-licensed CLI (Go + TypeScript). Restores from archive bytes plus the customer’s offline key alone — no vendor, no network, no API.

Authentication and Authorisation (no-custody)

  • No standing vendor access. The engine and console run under the customer’s keys in the customer’s account; Maelstrom holds no customer keys, data or Cloudflare tokens.
  • Operator access to the console is authenticated and authorised by the customer’s own identity provider within the customer’s tenant. Restore is a gated, approval-driven operation in the console.
  • Vendor diagnostics are consent-gated. Where Maelstrom must assist, the owner mints a time-boxed, scoped, immediately-revocable read-only pull credential alongside a redaction-safe signed support bundle. There is no standing inbound path and no standing vendor telemetry.
  • Licence tokens minted by the control-plane are the only vendor-issued credential, and the control-plane is fail-open so that licence or availability behaviour never blocks a backup or restore.

Encryption in Transit and at Rest

  • In transit: all control-plane and console-proxy traffic is TLS-terminated by Cloudflare (TLS 1.2+, TLS 1.3 preferred); the update channel is served over HTTPS.
  • At rest: archives are sealed with post-quantum hybrid encryption under customer-held keys (break-glass, operational and archive-signer keys, customer-generated via a guided in-browser key ceremony with a printed recovery sheet and offline-storage guidance). Because archives are sealed under customer-held keys, Maelstrom holds no key that can decrypt customer data.

Supply-Chain Authenticity (update channel)

  • Engine and console updates are published to a signature-pinned R2 bucket; the engine verifies each update’s signature against a pinned public key before applying.
  • Updates are pulled, never pushed; safe-apply is canary-gated with automatic rollback; there is no phone-home.
  • Releases are signed with the downpipes Update Signer, held offline by the operator and never deployed to any Worker; the downpipes Licence Signer lives in Cloudflare Secrets Store on the control-plane (fail-open).

Dogfooding

Maelstrom’s entire Cloudflare estate is backed up by Maelstrom’s own production instance of downpipes (we run downpipes as a customer of our own product). See Cloudflare Backup Evidence.


Evidence Artefacts

Configuration Files

provii-verifier:

  • provii-verifier/wrangler.toml. Cloudflare Workers configuration
  • provii-verifier/src/security/headers.rs. Security headers implementation
  • provii-verifier/src/security/auth.rs. HMAC authentication (ClientAuthenticator)
  • provii-verifier/src/durable_objects/nonce_do.rs. Nonce replay prevention

provii-issuer:

  • provii-issuer/wrangler.toml. Cloudflare Workers configuration
  • provii-issuer/src/security.rs. HMAC verification, API key auth

provii-management:

  • provii-management/wrangler.toml. Cloudflare Workers configuration
  • provii-management/src/index.ts. Main application with middleware stack
  • provii-management/src/middleware/auth.ts. HMAC + session authentication
  • provii-management/src/middleware/security-headers.ts. Security headers
  • provii-management/src/middleware/csrf.ts. CSRF protection
  • provii-management/src/middleware/audit.ts. Audit logging
  • provii-management/src/utils/validation.ts. Input validation

provii-credit-management:

  • provii-credit-management/wrangler.toml. Cloudflare Workers configuration
  • provii-credit-management/src/utils/security-headers.ts. Security headers
  • provii-credit-management/src/utils/validation.ts. Input validation

Documentation

Security Policies (in provii-verifier/docs/security/):

  • RATE_LIMITING.md (41 KB)
  • AUTHENTICATION_PATHWAYS.md (44 KB)
  • DATA_CLASSIFICATION.md (64 KB)
  • CRYPTOGRAPHIC_INVENTORY.md (85 KB)
  • KEY_MANAGEMENT_POLICY.md (51 KB)
  • VALIDATION_RULES.md (103 KB)

Compliance:

  • src/content/trust/compliance/requirements/unified-control-matrix.md. Control matrix
  • src/content/trust/compliance/requirements/evidence-mapping.md. Evidence mapping

Monitoring and Alerting

Audit Logs:

  • KV namespace: MANAGEMENT_AUDIT_LOGS
  • Retention: 90 days (configured in wrangler.toml and audit.ts); critical security event logs are retained for up to 365 days
  • Format: JSON structured logs (AuditEvent interface)

Workers Logs in Grafana Loki:

  • Real-time request metrics derived from structured JSON log lines
  • HTTP status code distribution
  • Geographic distribution
  • Rate limit violation trends

Control Mapping Summary

ControlDescriptionStatusEvidence Location
UC-044Encryption in Transit (TLS)✅ ImplementedCloudflare Universal SSL, HSTS headers in all services; downpipes: TLS on control-plane/console-proxy/update channel, archives PQ-sealed at rest under customer-held keys
UC-045Authentication Mechanisms✅ ImplementedHMAC-SHA256 (provii-verifier, provii-issuer, provii-management), JWT (provii-management), API keys (provii-issuer); downpipes: customer IdP for console, licence tokens, consent-gated pull credentials
UC-046Authorisation Controls✅ ImplementedRBAC (provii-management), BOLA protection (provii-verifier), ownership verification; downpipes: gated restore approvals in console; no standing vendor access
UC-047Incident Response✅ ImplementedAudit logging to KV, incident response playbooks; downpipes: in-tenant scored posture, hourly canary flights, consent diagnostics
UC-048Vulnerability Management✅ Implementednpm audit, cargo audit, Dependabot, Semgrep, Trivy, Gitleaks; downpipes: signature-pinned pull-only update channel, offline Update Signer
UC-049Security Monitoring✅ ImplementedRate limiting via per-service KV counters, Cloudflare Workers Logs (Grafana Loki); downpipes: routeless engine (no inbound surface), self-monitoring runs dark
UC-050Penetration Testing🟡 PlannedAnnual external pentest, responsible disclosure programme; downpipes control-plane + update channel in scope, in-tenant engine/console customer-operated (out of scope)
UC-051Security Awareness Training✅ ImplementedQuarterly training, annual security awareness day, security champions programme; module covers downpipes no-custody / runs-dark API design

Additional API Security Controls:

  • Security Headers (13 headers on provii-verifier, on all services)
  • Input Validation (Zod schemas for TypeScript, strict newtype wrappers for Rust)
  • CSRF Protection (Single-use tokens with session binding)
  • CORS Configuration (Strict origin validation)
  • Secret Management (Cloudflare Secrets Store)
  • Cryptographic Agility (Documented in CRYPTOGRAPHIC_INVENTORY.md)
  • No-Custody API Design (downpipes: routeless in-tenant engine, fail-open vendor control-plane, signature-pinned pull-only update channel, consent-gated diagnostics)

Recommendations

Short-Term (1-3 months)

  1. Implement CSP Reporting Dashboard:
  • Store CSP violation reports in KV/DO
  • Build visualisation dashboard
  • Set up alerting for unusual patterns
  1. Enhance Rate Limit Monitoring:
  • Deploy Grafana dashboards
  • Implement anomaly detection algorithms
  • Create runbooks for common attack patterns

Medium-Term (3-6 months)

  1. External Penetration Test:
  • Engage third-party security firm
  • Focus on OWASP API Security Top 10
  • Validate ASVS Level 3 compliance claims
  1. Automated Security Testing:
  • Integrate OWASP ZAP into CI/CD
  • Implement API fuzzing with RESTler
  • Set up continuous security scanning
  1. Advanced Threat Detection:
  • Deploy machine learning-based anomaly detection
  • Implement behavioural analysis for authentication attempts
  • Create automated incident response workflows

Long-Term (6-12 months)

  1. Post-Quantum Cryptography Migration:
  • Follow PQC migration roadmap
  • Implement hybrid cryptography (classical + PQC)
  • Monitor NIST PQC standardisation progress
  1. Zero Trust Architecture:
  • Implement mutual TLS for service-to-service communication
  • Deploy service mesh for microservices
  • Enhance identity verification at every layer
  1. Security Certification:
  • Pursue SOC 2 Type II certification
  • Pursue ISO 27001 certification when commercially justified
  • Complete PCI DSS compliance (if accepting card payments)

Conclusion

The Maelstrom AI backend infrastructure demonstrates a defence-in-depth approach to API security across both in-scope platforms — Provii and downpipes. For Provii, controls are implemented across all services and are designed to provide protection against common API vulnerabilities; the combination of Cloudflare’s edge security, HMAC authentication, RBAC authorisation, per-service KV counter rate limiting, and security headers is designed to address common API attack vectors. For downpipes, the API surface is minimised by design: the engine is routeless and runs dark, the vendor control-plane is fail-open and holds no customer keys, data or tokens, updates are pulled from a signature-pinned channel, and vendor diagnostics are consent-gated.

Compliance Status:

  • OWASP API Security Top 10 (2023): Controls implemented across all Provii services; downpipes minimises the API surface (routeless engine, fail-open control-plane)
  • OWASP ASVS 5.0.0 Level 3: Hardening fixes applied to provii-verifier
  • NIST SP 800-63B (Authentication): HMAC + PKCE authentication implemented (Provii)
  • GDPR (Data Protection): Audit logging and data minimisation designed to support GDPR obligations; downpipes adds no sub-processor for customer data (no-custody — Maelstrom holds no customer keys, data or tokens)

Evidence Collection Date: 2026-05-21 Last Updated: 2026-06-19 Next Review: 2026-11-21 (Quarterly) Document Owner: Security Lead Approver: ISMS Owner


Document Classification: Public Review Schedule: Quarterly Version: 2.1

Change History:

  • v2.1 (2026-06-19): downpipes added as second in-scope platform; the legacy backup worker retired in favour of self-hosted downpipes. Added downpipes API-surface coverage (routeless in-tenant engine, fail-open vendor control-plane, signature-pinned pull-only update channel, consent-gated diagnostics) across UC-044 through UC-051 and a dedicated downpipes API Surface section.
  • v2.0 (2026-05-21): API security evidence collection for UC-044 through UC-051.