Security

RAT implements defense-in-depth security across every layer: network, transport, authentication, authorization, input validation, SQL injection prevention, Python sandboxing, query enforcement, container hardening, and webhook security.


Security Architecture


Authentication

RAT supports three authentication modes, selected based on configuration:

Mode 1: Plugin Auth (Pro)

When the auth plugin is configured in rat.yaml, all incoming requests are authenticated via the plugin’s Authenticate gRPC call.

JWT validation details:

  • Signature verified against Keycloak’s JWKS endpoint
  • Token expiry checked (clock skew tolerance: 30 seconds)
  • Required claims: sub (user ID), email, realm_access.roles
  • Invalid or expired tokens return 401 Unauthorized

Mode 2: API Key Auth

When RAT_API_KEY environment variable is set, ratd validates the X-API-Key header against it:

X-API-Key: {RAT_API_KEY value}

This is a simple shared-secret model suitable for single-user or CI/CD integrations. The API key is compared using constant-time comparison to prevent timing attacks.

Mode 3: Noop Auth (Community Default)

When no auth plugin is configured and no API key is set, all requests pass through without authentication. This is the default for the Community Edition --- a single-user, self-hosted platform.

Auth Mode Resolution

1. If auth plugin configured in rat.yaml → Plugin Auth (JWT)
2. Else if RAT_API_KEY env var is set   → API Key Auth
3. Else                                 → Noop Auth (pass-through)

Rate Limiting

ratd implements per-IP token bucket rate limiting to prevent abuse and denial-of-service.

Default Limits

SettingValueDescription
Rate50 requests/secondSustained request rate per IP
Burst100 requestsMaximum burst above the sustained rate

How It Works

The rate limiter uses a token bucket algorithm:

  • Each IP address gets a bucket with 100 tokens (burst capacity)
  • Tokens refill at 50 per second (sustained rate)
  • Each request consumes 1 token
  • When the bucket is empty, requests are rejected with 429 Too Many Requests

Per-Endpoint Overrides

Certain endpoints have custom rate limits:

Endpoint PatternRate LimitReason
POST /api/v1/webhooks/*100 requests/minuteWebhook endpoints receive external traffic
GET /healthUnlimitedHealth checks should never be rate-limited

Rate Limit Headers

Rate-limited responses include standard headers:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708099200
Retry-After: 1

Input Validation

Path Parameter Validation

All path parameters (namespace, layer, pipeline name) are validated against strict patterns:

ParameterPatternMax LengthExamples
Namespace[a-z][a-z0-9_-]*128 charsdefault, ecommerce, my-project
Layerbronze, silver, goldexact matchbronze
Pipeline name[a-z][a-z0-9_-]*128 charsclean_orders, daily-revenue
Run IDUUID format36 chars550e8400-e29b-41d4-a716-446655440000

Invalid path parameters return 400 Bad Request with a descriptive error message. The path validation middleware (layer 11 in the chain) runs before any handler logic.

File Path Validation

File paths submitted via the storage API are validated to prevent directory traversal:

CheckRuleBlocked Example
No parent traversalReject .. anywhere in path../../etc/passwd
No absolute pathsReject paths starting with //etc/shadow
No null bytesReject \0 in pathfile.txt\0.jpg
Allowed characters[a-zA-Z0-9._-/] onlyfile;rm -rf /
File path validation (simplified)
func validateFilePath(path string) error {
    if strings.Contains(path, "..") {
        return errors.New("path traversal not allowed")
    }
    if filepath.IsAbs(path) {
        return errors.New("absolute paths not allowed")
    }
    if strings.ContainsRune(path, 0) {
        return errors.New("null bytes not allowed")
    }
    return nil
}

JSON Body Size Limit

The JSON body limiter middleware (layer 7) caps request bodies at 1 MB. Requests exceeding this limit receive 413 Payload Too Large. This prevents:

  • Memory exhaustion from large JSON payloads
  • Slow request processing
  • Denial-of-service via large uploads (file uploads use a separate multipart endpoint with its own limits)

SQL Injection Prevention

RAT prevents SQL injection at multiple levels:

Go (ratd) --- sqlc

All database queries in ratd use sqlc, which generates type-safe Go code from SQL. Queries are parameterized at compile time:

queries/pipelines.sql
-- name: GetPipeline :one
SELECT * FROM pipelines
WHERE namespace = $1 AND layer = $2 AND name = $3;
Generated Go code (sqlc)
func (q *Queries) GetPipeline(ctx context.Context, arg GetPipelineParams) (Pipeline, error) {
    row := q.db.QueryRow(ctx, getPipeline, arg.Namespace, arg.Layer, arg.Name)
    // ... type-safe scanning
}

There is zero string interpolation in any SQL query. All parameters are bound via $1, $2, etc.

Python (runner) --- Parameterized DuckDB

The runner uses parameterized queries for any user-influenced SQL:

Parameterized DuckDB query
# NEVER this:
# conn.execute(f"SELECT * FROM {table_name}")  ← SQL injection risk
 
# ALWAYS this:
conn.execute("SELECT * FROM iceberg_scan(?)", [table_path])

Python (ratq) --- Read-Only + Blocked Patterns

The query service adds a third layer: even if SQL injection were possible, the query would be blocked by the read-only enforcement (see below).


Python Sandbox

The runner executes Python pipelines (pipeline.py) using exec() with a 4-layer sandbox:

Layer 1: Blocked Builtins (26)

The following built-in functions are removed from the execution namespace:

Blocked builtins
BLOCKED_BUILTINS = {
    'exec', 'eval', 'compile',           # Code execution
    '__import__', 'importlib',            # Import system
    'open', 'input', 'print',            # I/O
    'globals', 'locals', 'vars', 'dir',  # Introspection
    'getattr', 'setattr', 'delattr',     # Attribute manipulation
    'type', 'super', 'classmethod',      # Class system
    'staticmethod', 'property',
    'breakpoint', 'exit', 'quit',        # Debugger/exit
    'help', 'license', 'credits',        # Interactive
    'memoryview', 'bytearray',           # Raw memory
}

Layer 2: Blocked Imports (30+)

Imports are intercepted and blocked for dangerous modules:

Blocked imports (selection)
BLOCKED_IMPORTS = {
    'os', 'sys', 'subprocess',           # System access
    'socket', 'http', 'urllib',          # Network access
    'shutil', 'pathlib', 'glob',         # Filesystem access
    'pickle', 'shelve', 'marshal',       # Deserialization
    'ctypes', 'cffi',                    # Native code
    'threading', 'multiprocessing',      # Concurrency
    'signal', 'resource',               # System resources
    'importlib', 'pkgutil',             # Import manipulation
    'code', 'codeop', 'compileall',     # Code compilation
    'ast',                               # AST manipulation
    'inspect', 'dis',                    # Code inspection
    # ... and more
}

Layer 3: AST Validation

Before execution, the Python code is parsed into an Abstract Syntax Tree (AST) and validated:

  • No import os, import sys, etc. (catches static imports that bypass the import hook)
  • No __builtins__ access (prevents sandbox escape via __builtins__.__import__)
  • No __class__, __subclasses__, __globals__ access (prevents class hierarchy traversal)
  • No exec(), eval(), compile() calls (prevents nested code execution)

Layer 4: SQL Filtering

Any SQL strings constructed inside the Python pipeline are filtered through the same safety checks used by the query service. This prevents a Python pipeline from executing dangerous SQL via DuckDB.

⚠️

The Python sandbox is designed for defense-in-depth, not as a complete security boundary. In multi-tenant environments, use the Pro container-executor plugin, which runs each pipeline in an isolated container with its own network namespace and filesystem.


Query Service Read-Only Enforcement

The query service (ratq) enforces strict read-only access. Before executing any SQL, it scans for blocked patterns:

Blocked SQL Statements (25+)

-- Data Definition Language (DDL)
CREATE, ALTER, DROP, TRUNCATE, COMMENT, RENAME
 
-- Data Manipulation Language (DML)
INSERT, UPDATE, DELETE, MERGE, UPSERT
 
-- Data Control Language (DCL)
GRANT, REVOKE
 
-- Transaction Control
BEGIN, COMMIT, ROLLBACK, SAVEPOINT
 
-- System Commands
SET, PRAGMA, LOAD, INSTALL, ATTACH, DETACH,
COPY, EXPORT, IMPORT, VACUUM, CHECKPOINT, CALL

Blocked Functions (20+)

-- File I/O
read_csv, read_csv_auto, read_json, read_json_auto,
read_parquet, write_csv, write_parquet, write_json
 
-- System functions
read_blob, write_blob
 
-- HTTP functions
http_get, http_post
 
-- Extension management
install_extension, load_extension

Query Limits

LimitValuePurpose
Max query size100 KBPrevent memory exhaustion from huge queries
Query timeout30 secondsPrevent long-running queries from blocking
Result set limitConfigurableCap the number of returned rows

Enforcement Order

1. Size check (< 100 KB)
2. Statement blocklist scan
3. Function blocklist scan
4. Execute with 30s timeout
5. Return results

If any check fails, the query is rejected immediately with a descriptive error.


Container Hardening

Every container in the RAT stack is hardened with the following Docker security features:

Security Features by Container

Featureratdrunnerratqportalpostgresminionessie
Read-only filesystemYesYesYesYesNoNoNo
tmpfs /tmpYesYesYesYes---------
cap_drop ALLYesYesYesYesYesYesYes
no-new-privilegesYesYesYesYesYesYesYes
pids_limit 100YesYesYesYesYesYesYes
Non-root userYesYesYesYesYesYesYes
Memory limits512M2G1G512M1G1G512M
CPU limits1.02.01.01.01.01.01.0

Postgres, MinIO, and Nessie cannot use read-only filesystems because they need to write data to disk. They use Docker volumes for persistent storage, and all other filesystem paths are ephemeral.

Explanation of Each Feature

FeatureWhat It Prevents
Read-only filesystemWriting to the container filesystem (malware persistence, config tampering)
tmpfs /tmpProvides a writable /tmp in RAM only (wiped on restart)
cap_drop ALLDrops all Linux capabilities (no raw sockets, no chown, no mount, etc.)
no-new-privilegesPrevents privilege escalation via setuid/setgid binaries
pids_limit 100Prevents fork bombs (max 100 processes per container)
Non-root userContainers run as unprivileged users (ratd: scratch, runner: appuser, etc.)
Memory limitsPrevents runaway memory consumption (OOM killer triggers at limit)
CPU limitsPrevents CPU monopolization by a single container

Logging

All containers use the json-file logging driver with rotation:

Logging configuration
logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

This caps log storage at 30 MB per container (3 files x 10 MB), preventing disk exhaustion from verbose logging.


Webhook Security

RAT supports webhook triggers (e.g., trigger a pipeline run via an external HTTP call). Webhook security follows these principles:

Token Handling

  • Webhook tokens are stored as SHA-256 hashes in the database (never plaintext)
  • Token comparison uses constant-time comparison (prevents timing attacks)
  • Tokens are transmitted in the X-Webhook-Token header only (never in the URL query string)
Constant-time token comparison
expectedHash := sha256.Sum256([]byte(storedToken))
receivedHash := sha256.Sum256([]byte(receivedToken))
if !hmac.Equal(expectedHash[:], receivedHash[:]) {
    return errors.New("invalid webhook token")
}

Why Header-Only?

Tokens in URL query strings:

  • Appear in server access logs
  • Appear in browser history
  • Appear in HTTP referer headers
  • May be cached by proxies

By requiring tokens in the X-Webhook-Token header, these exposure vectors are eliminated.

Rate Limiting

Webhook endpoints have a stricter rate limit: 100 requests/minute (vs. 50 requests/second for normal API calls). This prevents abuse from external callers while still allowing reasonable trigger frequency.


Network Segmentation

Network Rules

SourceCan ReachCannot Reach
Browser/ExternalPortal (3000), ratd (8080)runner, ratq, postgres, minio, nessie
Portalratd (internal)runner, ratq (goes through ratd)
ratdAll backend servicesN/A (has full access)
runnerMinIO, Nessie, ratd (callback)Postgres, ratq, portal
ratqMinIO, NessiePostgres, runner, portal, ratd
postgresNone (accepts connections only)All outbound
⚠️

In development mode, Postgres (5432), MinIO (9000, 9001), and Nessie (19120) bind to localhost only (127.0.0.1:port:port). These ports are accessible from the host machine for debugging but not from external networks. In production, remove these localhost port bindings entirely.


Security Headers

ratd sets the following security headers on all responses:

HeaderValuePurpose
X-Content-Type-OptionsnosniffPrevents MIME type sniffing
X-Frame-OptionsDENYPrevents clickjacking
X-XSS-Protection1; mode=blockEnables browser XSS filter
Referrer-Policystrict-origin-when-cross-originControls referrer information
X-Request-Id{uuid}Unique request identifier for tracing

Audit Logging

All state-changing operations (POST, PUT, DELETE) are logged to the audit_log table:

audit_log schema
CREATE TABLE audit_log (
    id          UUID PRIMARY KEY,
    actor       VARCHAR(255) NOT NULL,  -- username or "anonymous"
    action      VARCHAR(100) NOT NULL,  -- "pipeline.create", "run.trigger"
    object_type VARCHAR(20),            -- "pipeline", "run", "schedule"
    object_id   UUID,
    details     JSONB,                  -- request body, response code, etc.
    timestamp   TIMESTAMPTZ NOT NULL
);

Audit Events

ActionTrigger
pipeline.createNew pipeline created
pipeline.updatePipeline code or config modified
pipeline.deletePipeline soft-deleted
run.triggerPipeline run started (manual, scheduled, triggered)
run.cancelPipeline run cancelled
schedule.createNew schedule created
schedule.updateSchedule modified
schedule.deleteSchedule deleted
namespace.createNew namespace created
storage.uploadFile uploaded to landing zone

Audit Log Retention

The reaper prunes audit log entries older than 365 days (configurable via audit_log_max_age_days in platform settings).