Skip to content

Outpost Advanced Security Configuration

This guide covers advanced security features for Arbitex Hybrid Outpost deployments that go beyond the baseline hardening covered in Outpost Security Hardening. These features provide network-layer access control, audit trail integrity, response security headers, and request validation. All settings are configured via environment variables and take effect without an outpost restart when applied through the admin config API.


The IP allowlist restricts which client IP addresses can reach the outpost proxy endpoint. When enabled, only requests originating from configured CIDR ranges are allowed — all others receive a 403 Forbidden response. This provides a network-layer defense independent of authentication.

Variable Type Default Description
IP_ALLOWLIST_ENABLED bool false Enable IP-based allowlisting. When true, only requests from IP_ALLOWLIST_CIDRS are accepted.
IP_ALLOWLIST_CIDRS string "" Comma-separated CIDR ranges (e.g. 10.0.0.0/8,192.168.1.0/24).
IP_ALLOWLIST_ADMIN_EXEMPT bool true When true, requests to /admin/* endpoints bypass the IP allowlist.

The middleware extracts the client IP using the following precedence:

  1. X-Forwarded-For header — the first IP address in the header value is used. This is the standard behavior when the outpost sits behind a load balancer or reverse proxy.
  2. Direct connection — if no X-Forwarded-For header is present, the IP address from the TCP connection (scope["client"]) is used.

When a request is denied by the IP allowlist, the outpost returns:

HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"detail": "IP address not in allowlist",
"client_ip": "192.168.1.xxx"
}

The client IP in the response is masked — the last octet of an IPv4 address (or the last group of an IPv6 address) is replaced with xxx. This prevents information leakage while still providing enough context for the client to understand the denial.

Each denied request increments an internal counter accessible via the admin API.

GET /admin/api/ip-allowlist/status

Returns the current IP allowlist configuration and denial statistics:

{
"enabled": true,
"cidrs": ["10.0.0.0/8", "192.168.1.0/24"],
"admin_exempt": true,
"denied_count": 42
}
Field Type Description
enabled bool Whether IP allowlisting is currently active.
cidrs list[string] Configured CIDR ranges.
admin_exempt bool Whether admin endpoints bypass the allowlist.
denied_count int Total number of denied requests since the outpost started.

Internal-only deployment (RFC 1918 ranges):

IP_ALLOWLIST_ENABLED=true
IP_ALLOWLIST_CIDRS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
IP_ALLOWLIST_ADMIN_EXEMPT=true

This allows all private network addresses while blocking any traffic from public IP ranges. Suitable when the outpost is deployed on an internal network segment.

VPN-only access:

IP_ALLOWLIST_ENABLED=true
IP_ALLOWLIST_CIDRS=10.8.0.0/16
IP_ALLOWLIST_ADMIN_EXEMPT=true

Restricts proxy access to the VPN subnet only. Adjust the CIDR to match your VPN address pool.

Specific application server IPs:

IP_ALLOWLIST_ENABLED=true
IP_ALLOWLIST_CIDRS=10.1.2.10/32,10.1.2.11/32,10.1.2.12/32
IP_ALLOWLIST_ADMIN_EXEMPT=true

Restricts access to specific known application servers. Use /32 for individual IPs. This is the most restrictive configuration and is recommended when the set of clients is fixed and known.

After configuring the IP allowlist, verify the configuration is active and correct:

Terminal window
# Check allowlist status
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
https://outpost:8301/admin/api/ip-allowlist/status
# Test from an allowed IP — should return 200
curl -H "Authorization: Bearer $API_KEY" \
https://outpost:8300/v1/chat/completions \
-d '{"model":"gpt-4","messages":[{"role":"user","content":"test"}]}'
# Test from a non-allowed IP — should return 403
# (run from a machine outside the configured CIDR ranges)
curl -v https://outpost:8300/v1/chat/completions

Monitor the denied_count field in the status response to track how many requests have been denied since the outpost started. A steadily increasing denied count with no corresponding user complaints typically indicates automated scanning or misconfigured clients.

IP allowlist settings are hot-reloadable. After updating the environment variables, apply the new configuration without restarting the outpost:

POST /admin/api/config/apply

The middleware reads configuration at request time from the settings object, so changes take effect on the next request after config/apply completes.


Request body hash logging provides an audit trail integrity mechanism by computing an HMAC hash of the original request (and optionally response) body before any DLP processing occurs. This creates a cryptographic chain-of-custody record — auditors can verify that the body recorded in the audit trail matches the original content sent by the client.

Variable Type Default Description
BODY_HASH_LOGGING_ENABLED bool false Enable HMAC hashing of request/response bodies for audit trail integrity.
BODY_HASH_ALGORITHM string sha256 Hash algorithm: sha256 or sha512.
BODY_HASH_LOG_RESPONSE bool false When true, also hash response bodies in audit events.

Body hash logging solves a specific compliance problem: proving that the content recorded in audit events matches the original content that was submitted. When DLP scans redact or modify content, the pre-scan body hash provides proof of the original submission.

The hash is computed as an HMAC (keyed hash), not a plain hash. This prevents an attacker with write access to audit logs from recomputing hashes for modified content — without the HMAC key, forged hashes are detectable.

The HMAC key is selected using the following precedence:

  1. AUDIT_HMAC_KEY — the dedicated audit HMAC key (preferred).
  2. ADMIN_JWT_SECRET — falls back to the admin JWT secret if no dedicated HMAC key is set.

When body hash logging is enabled, the following fields are added to each DLP scan audit event:

Field Present When Description
request_body_hash Always (when enabled) HMAC hash of the original request body before DLP processing.
response_body_hash BODY_HASH_LOG_RESPONSE=true HMAC hash of the response body from the upstream provider.

Hash values are hex-encoded strings. SHA-256 produces 64 characters; SHA-512 produces 128 characters.

GET /admin/api/body-hash/config

Returns the current body hash logging configuration:

{
"enabled": true,
"algorithm": "sha256",
"log_response": false,
"key_source": "AUDIT_HMAC_KEY"
}
Field Type Description
enabled bool Whether body hash logging is active.
algorithm string Hash algorithm in use (sha256 or sha512).
log_response bool Whether response bodies are also hashed.
key_source string Which key is being used: AUDIT_HMAC_KEY or ADMIN_JWT_SECRET.

To verify that body hash logging is working correctly:

  1. Enable body hash logging in the outpost configuration.
  2. Send a test request through the outpost proxy.
  3. Retrieve the audit event from GET /admin/audit/recent.
  4. Verify the hash field — the request_body_hash field should be present in the audit event.
  5. Compute the expected hash — using the same HMAC key and algorithm, compute the hash of the original request body and compare it to the recorded hash.
Terminal window
# Compute expected SHA-256 HMAC for verification
echo -n '{"model":"gpt-4","messages":[{"role":"user","content":"test"}]}' | \
openssl dgst -sha256 -hmac "$AUDIT_HMAC_KEY" -hex

If the computed hash matches the request_body_hash in the audit event, the chain-of-custody is intact — the recorded body hash corresponds to the original request content.

Body hashes are one-way HMAC digests. The original body content cannot be recovered from the hash. Body content itself is never written to audit logs — only the cryptographic fingerprint is recorded. This satisfies audit trail requirements without introducing additional data exposure risk.


The outpost applies a fixed set of security headers to every HTTP response via the SecurityHeadersMiddleware. These headers are not configurable — they are always applied and cannot be disabled.

Header Value Purpose
Strict-Transport-Security max-age=31536000; includeSubDomains Forces HTTPS for 1 year. Browsers that have visited the outpost will refuse plaintext HTTP connections.
X-Content-Type-Options nosniff Prevents browsers from MIME-sniffing the response body. The declared Content-Type is authoritative.
X-Frame-Options DENY Prevents the outpost response from being embedded in an iframe. Mitigates clickjacking.
Content-Security-Policy default-src 'none' Blocks all resource loading. The outpost serves JSON API responses only — no scripts, styles, or images should be loaded.
Referrer-Policy no-referrer Suppresses the Referer header on outgoing requests from any page that loaded an outpost response.

These values are derived from the outpost source code (outpost/security_headers.py). They are applied at the ASGI middleware layer and cannot be overridden by route handlers.

In addition to the five security headers above, the outpost generates a unique X-Request-ID header on every response:

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Characteristics:

  • Generated per request as a UUID v4. Each request receives a unique identifier.
  • Stored in scope state (scope["state"]["request_id"]) so downstream handlers and middleware can access it.
  • Emitted as a response header on every HTTP response.
  • Propagated to audit events — the request_id field in DLP scan audit events matches the X-Request-ID header.
  • Included in structured JSON logs as request_id — enables log correlation across middleware, route handlers, and audit events.

Use the X-Request-ID value to trace a single request through outpost logs, audit events, and SIEM exports.


The outpost validates incoming requests at two layers: a middleware layer that checks Content-Length headers, and a route-level layer that reads actual body bytes.

Middleware Layer: Content-Length Validation

Section titled “Middleware Layer: Content-Length Validation”
Variable Type Default Description
REQUEST_MAX_BODY_BYTES int 1048576 (1 MB) Maximum allowed Content-Length for any request. Requests exceeding this limit are rejected before the body is read.

When a request’s Content-Length header exceeds the configured limit, the middleware returns:

HTTP/1.1 413 Request Entity Too Large
Content-Type: application/json
{
"detail": "Request body too large"
}

This is a fast-path rejection — the body is not read from the socket, saving bandwidth and CPU.

The middleware enforces Content-Type: application/json on all POST, PUT, and PATCH requests. Requests with a missing or non-JSON content type receive a 415 Unsupported Media Type response.

Exempt paths: The following health and readiness endpoints are exempt from content-type enforcement:

  • /health
  • /ready
  • /live
  • /metrics

These endpoints may receive requests without a Content-Type header (e.g. from Kubernetes liveness probes).

The /v1/chat/completions endpoint performs a second body size check by reading the actual body bytes. This catches cases where the Content-Length header is absent, spoofed, or the transfer uses chunked encoding.

Variable Type Default Description
MAX_REQUEST_BODY_MB int 10 Maximum allowed actual body size in megabytes for /v1/chat/completions.

The route-level check reads the full body into memory and compares the byte count against the configured limit. If the body exceeds the limit:

HTTP/1.1 413 Request Entity Too Large
Content-Type: application/json
{
"error": {
"message": "Request body exceeds 10MB limit",
"type": "request_too_large"
}
}

The outpost registers middleware in the following order (outermost to innermost):

  1. SecurityHeadersMiddleware — adds security headers and X-Request-ID to all responses.
  2. IPAllowlistMiddleware — blocks requests from non-allowed IPs (when enabled).
  3. RequestValidationMiddleware — enforces body size and content-type rules.

A request passes through these layers in order. A denied IP never reaches the request validation layer. A request that passes IP validation but exceeds body size limits is rejected before reaching route handlers.


This checklist combines all items from the pre-deployment checklist in the base hardening guide with the advanced security features documented in this guide. Complete all items before placing the outpost in production.

  • TLS certificate paths are valid. Confirm TLS_SERVER_CERT_PATH and TLS_SERVER_KEY_PATH exist and are readable by the outpost process. Verify the certificate chain: openssl verify -CAfile <ca.pem> <server.pem>.
  • Certificate expiry is at least 30 days from deployment. Check cert_expiry_days via GET /admin/api/sync-status after startup. Schedule rotation before the renewal threshold.
  • mTLS client CA is configured if TLS_VERIFY_CLIENT=true. Confirm TLS_CA_CERT_PATH contains the correct CA certificate and test with a client certificate before enabling.
  • OUTPOST_EMERGENCY_ADMIN_KEY is set and non-empty. Generate with openssl rand -hex 32. An empty key disables the emergency fallback.
  • AUDIT_HMAC_KEY is set. The outpost will not start without this key. Verify it is populated in the secrets store.
  • POLICY_HMAC_KEY is set. Required for policy bundle integrity verification. Do not set INSECURE_SKIP_HMAC=true in production.
  • RS256 JWKS URL or RSA public key is configured if OAuth proxy authentication is required. Confirm OAUTH_JWT_PUBLIC_KEY or OAUTH_JWKS_URL is set and the endpoint is reachable.
  • Rate limits are appropriate for expected traffic. Default limits (60 rpm proxy, 30 rpm admin) are conservative. Adjust RATE_LIMIT_REQUESTS_PER_MINUTE and RATE_LIMIT_BURST for your workload.
  • REQUEST_MAX_BODY_BYTES is set appropriately. Default 1 MB. Increase if your application sends large prompts. Do not disable.
  • MAX_REQUEST_BODY_MB is set appropriately. Default 10 MB. This is the hard limit on actual body bytes for /v1/chat/completions.
  • IP allowlist is enabled if the outpost should only accept traffic from known sources. Set IP_ALLOWLIST_ENABLED=true and configure IP_ALLOWLIST_CIDRS.
  • CIDR ranges are minimal. Only include the specific subnets or IPs that need access. Avoid overly broad ranges (e.g. 0.0.0.0/0).
  • Admin exemption is intentional. IP_ALLOWLIST_ADMIN_EXEMPT=true (default) allows admin access from any IP. If your admin port is network-accessible, consider setting this to false and including admin IPs in the CIDR list.
  • Reverse proxy X-Forwarded-For is configured correctly. The outpost trusts the first IP in X-Forwarded-For. Ensure your proxy sets (not appends to) this header.
  • Audit logging path is writable. The SQLite audit database must be on a persistent volume. Ephemeral storage causes audit loss on pod restart.
  • Body hash logging is enabled if chain-of-custody is required. Set BODY_HASH_LOGGING_ENABLED=true and configure BODY_HASH_ALGORITHM.
  • Dedicated AUDIT_HMAC_KEY is set for body hash logging. Do not rely on the ADMIN_JWT_SECRET fallback in production.
  • SIEM export is configured if required by compliance policy. Configure SIEM_DIRECT_URL and SIEM_DIRECT_TOKEN. Verify connectivity from the outpost network segment.
  • LOG_LEVEL is set to info. Debug logging may expose request headers and policy evaluation details.
  • Air-gap mode is configured if the outpost cannot reach PLATFORM_MANAGEMENT_URL. Set OUTPOST_AIRGAP=true and configure AIRGAP_POLICY_PATH.