Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Security architecture and boundaries

Vision preserved: unfinished enterprise controls and absolute former claims remain visible, with a recommendation for each, in the capability ledger.

rullst-security contains composable controls for HTTP applications. The crate does not install every control automatically and does not replace secure domain logic, a trusted reverse proxy, operating-system hardening, or independent security testing. To choose between overlapping Core and rullst-security layers, and for one recommended production stack, see which security layer to use, and when. The security assurance case connects these boundaries to the threat models, secure design principles and tested countermeasures.

Defense in depth

flowchart LR
    Peer[Trusted peer identity] --> Edge[Rate limit and honeypots]
    Edge --> Request[CSRF, CSWSH and bounded RASP/WAF]
    Request --> Identity[Authentication, MFA and RBAC]
    Identity --> App[Application and parameterized data access]
    App --> Response[Secure headers and supported DLP filtering]
    App --> Audit[Tamper-evident audit records]

Each arrow is an application integration point. If a layer is not mounted, its counter and API being present in the crate do not protect traffic.

Cloud and VPS deployments

The application controls work in a compatible cloud container or VPS just as they do in a local HTTP server. Their effect depends on the configured routes, middleware, identity and persistence. Server mounts the Core baseline; additional Security controls require explicit composition.

ScopeCurrent boundary
Sites and API backendsBounded WAF/RASP inspection, HTML sanitization, CSP/security headers, login abuse controls, ownership/role helpers, WebSocket-origin validation and audit primitives are available. Application policy and negative route tests remain necessary.
Multiple application instancesThe opt-in Redis limiter has an atomic shared-budget contract. Production must require distributed mode explicitly. Process-local jails, bans and counters do not become shared because the application has more replicas.
Reverse proxyVerified socket identity is the default. Core’s TrustedProxyLayer (Server::trusted_proxies or [security] trusted_proxies) accepts a forwarded address only from listed proxy networks; an arbitrary client header cannot establish the rate-limit or ban identity.
Host and cloud accountOS/SSH patching, firewall rules, IAM, secret custody, backup/restore and privileged container configuration belong to the deployment operator/provider. The crate does not administer these systems.
Network availabilityApplication request limits can reduce specific abuse. Network saturation and upstream DDoS filtering require infrastructure controls outside the application process.

The v13 deployment candidate exercises two real application processes with an existing proxy, shared Redis budgets, forged forwarding metadata and HTTP/WebSocket controls. Local acceptance passes, and hosted workspace and installed-archive checks passed in PR #224; the final release campaign remains separate. It does not certify a cloud account, VPS or production topology.

The separate v13 local configuration diagnostic, cargo rullst deploy:doctor, reuses Core validation with explicit environment-source selection and bounded, redacted reports. It identifies what was actually inspected and lists unobserved controls; its source merged in PR #225 after hosted checks, and its archive gate passed in a later exact-commit run. It does not label configuration as deployed evidence, modify firewall/SSH/cloud settings or certify a host. No new security crate is introduced. See the delivery plan.

Canonical production preset

ProductionPreset::middleware_order() is the machine-readable ordering contract (introduced in v12). From the outermost inbound boundary to the handler, the order is:

  1. trusted-proxy policy;
  2. request-body limit;
  3. request ID;
  4. tracing;
  5. secure headers;
  6. explicit CORS allowlist;
  7. bounded WAF/RASP;
  8. CSRF;
  9. session validation;
  10. authentication;
  11. tenant membership resolution;
  12. role, permission and object-ownership authorization;
  13. identity/direct-peer rate limit;
  14. application handler.

Tower response flow unwinds in reverse, so the secure-header layer still observes and protects responses returned by the inner guards and handler. Server mounts the framework-owned staging/production baseline. Session, authentication, tenant membership and authorization are deliberately application-owned: the framework cannot infer those policies without creating an insecure universal default. Generated applications must mount those layers in the declared slots, and protected parameterized routes must still perform an object-level ownership check.

The direct socket peer is the default network identity. A deployment must not accept Forwarded or X-Forwarded-For until its exact proxy hops are configured and tested. Core implements the first stage as TrustedProxyLayer, which Server::trusted_proxies mounts outside the security baseline, lifecycle, Traffic Shield and rate limiter in both the static and hot-reload servers (see identity and network trust). Body limits must be outer to any middleware that buffers content. Webhook routes may bypass browser CSRF only by exact path and only when their provider signature middleware is mandatory.

Academy production-boundary diagnostic

ProductionPreset::academy() layers twelve Academy-specific integration requirements over the canonical middleware order. Missing observations become NOT_EVALUATED, duplicate observations are rejected, and validation succeeds only when every requirement has one explicit PASS. The contract covers identity, school membership, entitlements, object authorization, tenant isolation, assessments, score events, durable automation/audit, content safety, privacy and distributed abuse controls. It cannot inspect or certify a deployed topology by itself.

cargo rullst academy:doctor emits the normalized contract in text or JSON and returns a failing exit status until all requirements pass. An evidence document uses the versioned rullst.academy-evidence.v1 schema:

{
  "schema_version": "rullst.academy-evidence.v1",
  "checks": [
    {
      "requirement": "authenticated_identity",
      "status": "PASS",
      "evidence": ["test:session_rejects_forged_identity"]
    }
  ]
}

Run it with cargo rullst academy:doctor --evidence academy-evidence.json --json. Omitted checks remain NOT_EVALUATED, and a PASS without a non-empty evidence reference is invalid. Evidence strings are caller declarations rather than independently verified proof. The output therefore always includes certification: false; satisfying this contract is not an audit, grade or production-readiness claim. Process-level CLI tests cover both the incomplete failing report and a complete declared-evidence report.

Parameterized-route access contract

cargo rullst audit --idor requires every recognized parameterized route to carry an adjacent // rullst-access: public|owner|role|admin — reason marker. public is accepted only for a recognized GET route. owner requires RbacGuard::authorize_owner_or_role; role requires a recognized role guard; and admin requires RequireRoleLayer or NexusAuthPolicy::protect_router. The latter lets application operational routes reuse the same fail-closed peer/credential and administrator boundary as Nexus. The guard is looked up in the same crate’s src tree, outside comments and #[cfg(test)] items; it need not be in the route’s file.

The marker records intent and the scanner catches common omissions; neither is a proof of domain ownership. Protected object routes still need negative HTTP tests in which an authenticated subject requests another subject’s resource and receives a denial before data or side effects are exposed.

Control map

Risk areaAvailable primitivesBoundary
Broken access controlRbacGuard, ownership helpers, authenticated user context.The application must apply a check to every protected object and operation.
Cryptographic storageAES-256-GCM field encryption and zeroizing secret wrappers.Operators own key generation, storage, rotation, separation, and recovery.
InjectionSQLx binds, strict identifier validation, sanitizer and bounded RASP patterns.Heuristics are not a complete language parser; domain validation and parameterization remain mandatory.
MisconfigurationNonce-based CSP and a strict HTTP-header baseline.Proxies and page content change the deployed policy; no scanner grade is guaranteed.
Authentication abuseLogin jail, local limiter, optional atomic Redis limiter, timing helpers, TOTP, subject-bound recovery-code verifiers and WebAuthn integration.Real Redis topology/failover, durable transactional recovery consumption and UX, RP/origin configuration, trusted peer identity, and capacity planning remain application concerns.
Data integrityHMAC audit records plus an opt-in bounded HMAC-chained local SIEM journal with named rotation keys.Whole-tail rollback detection, independent checkpoints, multi-writer operation, key protection, retention and remote delivery require external storage and operations.
Data leakageText-aware DLP, PII masking, and log redaction helpers.Unsupported content types, encodings, streams, and oversize bodies follow explicit policy and must be tested. Core response masking buffers at most 2 MiB of identity-encoded text and scans it in linear time; it is a heuristic for emails and 13-19 digit card-like runs, not complete PII detection. JSON responses are masked only inside string values, so numbers such as millisecond timestamps and 64-bit IDs keep their digits and the body stays valid JSON; other textual responses, including JavaScript, are masked as plain text. A 206 Partial Content response that masking would change is replaced by a 502 with Cache-Control: no-store, because a masked range no longer matches its Content-Range; a clean range passes through unchanged. Core masking and DlpResponseLayer split a multipart/byteranges response into its parts, check each textual identity-encoded part with the masker of its own media type and withhold the response the same way when a part would be masked or the body cannot be split exactly (an invalid boundary or delimiter line, more than 256 parts, a part header block over 8 KiB or no close delimiter). Each range is inspected on its own, so neither layer recognizes a value split across separately requested ranges, and Core does not recognize PII in a JSON range that starts inside a string literal; tower-http ServeDir, which serves Core static files, answers multi-range requests with 416 and never emits multipart/byteranges. An e-mail match needs an alphabetic top-level domain that is not a static-asset extension, so versioned package URLs ([email protected]) and density-suffixed asset names ([email protected]) are left unchanged.
AI input riskPrompt-injection heuristics and PII masking in the high-level AI client.No heuristic can prove a prompt safe or guarantee detection of every secret.

This is a control mapping, not a claim of complete OWASP Top 10 coverage.

Identity and network trust

Rate limiting, bans, honeypots, and audit attribution use the direct peer address unless a trusted-proxy policy explicitly accepts forwarded metadata. Core’s RateLimiter keys IPv4 peers per address and IPv6 peers per /64, so rotating addresses inside one delegated prefix shares a bucket. Its process-local map tracks at most 100,000 keys: fully refilled buckets are dropped and, beyond the cap, the least recently used buckets are evicted and restart with a full burst. Security’s rate_limit_middleware groups peers the same way; its table tracks at most 16,384 keys and fails closed for new keys while it is full. Never trust X-Forwarded-For, tenant headers, or role headers directly from an arbitrary client. Tenant membership and roles must come from an authenticated session or a cryptographically trusted internal gateway.

Trusted-proxy client resolution

rullst_core::security::TrustedProxyLayer is the explicit policy for deployments behind reverse proxies. TrustedProxyConfig::new accepts at most 64 IPv4/IPv6 addresses or CIDR networks; it rejects /0, host bits and duplicates, and normalizes IPv4-mapped IPv6 entries. An empty policy never reads forwarding headers. List only the networks your own proxies connect from: any host inside a listed network can choose the client address.

  • Only when the socket peer is trusted does the layer read exactly one header: X-Forwarded-For (default) or RFC 7239 Forwarded (with_header). The two are never merged, and header lines form one list in their received order.
  • The list is walked right to left, skipping trusted proxies; the first untrusted address is the client, and if every hop is trusted the left-most is used. Text to the left of the client entry is never parsed, so a client cannot choose its identity or force a fallback by padding its own header.
  • Ports, IPv6 brackets and IPv4-mapped addresses are normalized. unknown, obfuscated _identifiers, unquoted RFC 7239 IPv6/ports, duplicate parameters and other malformed entries are rejected. At most 4,096 bytes and 32 entries are examined from the right.
  • A missing, malformed, oversized or over-long header from a trusted peer keeps the socket peer, never fails the request and emits one tracing event (debug when missing, otherwise warn with the reason) naming the header and peer but never the header contents.
  • On success the request’s ConnectInfo<SocketAddr> becomes the client IP with port 0, so every existing consumer (Core and Security rate limiters, honeypots, the error console and Nexus lockout) uses it unchanged. Every request with peer metadata also receives a ClientAddr extension (client IP, original peer, via_trusted_proxy), which has no public constructor.
  • trust_forwarded_proto(true) additionally reports the scheme from that trusted peer as ClientAddr::forwarded_proto: one unambiguous X-Forwarded-Proto value, or the proto= of the selected Forwarded element. Nexus accepts an https report as TLS evidence for Basic Auth. Enable it only when every trusted proxy overwrites the value.

The hot-reload development server now also supplies ConnectInfo from the accepted socket. The layer does not authenticate a proxy beyond its network address, cannot detect a trusted proxy that forwards client-supplied headers unchanged, and does not replace authenticated per-user limits or distributed rate-limit state.

Request and response inspection

RASP/WAF rules are bounded to avoid uncontrolled CPU or memory work. They can reject known suspicious patterns in supported URI, header, and bounded body data, but application queries must still use binds and access control. Body media types are classified at least as broadly as axum’s extractors: any ASCII case, parameters, a json/xml subtype or +json/+xml suffix, and any type that starts with application/x-www-form-urlencoded. Core’s WAF checks the percent-decoded request path for traversal signatures only (../, ..\, /etc/passwd, win.ini), since routers decode path parameters; its SQL, XSS and command signatures apply to the query, the Referer, each cookie pair and bounded bodies (JSON key by key and string by string). SQL and shell signatures match injection structure, such as a quote breakout, a chained statement, union select or a metacharacter before a command name, never a keyword alone, so ordinary text like “select an option” passes.

DLP modifies only supported textual responses whose body can be safely buffered within configured limits. Applications must test JSON, HTML, binary, compressed, SSE, streaming, and oversized responses so headers and bodies remain protocol-correct. Masking and log redaction run in time linear in the inspected length. Database URL passwords are masked only inside the URL authority, which ends at the first /, ?, #, whitespace, quote, <, >, backtick or control character, or after 2,048 bytes; unencoded delimiters in a password therefore end detection early.

Cryptography providers

Rullst uses two cryptography libraries, each with one role (since 13.0.0-alpha.4):

ComponentLibraryWhy
Every TLS connection Rullst makes: outbound HTTPS (reqwest: AI and payment providers, OAuth, webhooks, S3 storage, Studio’s remote poller), SQLx (PostgreSQL and MySQL), Redis (Redis Streams messaging, ORM Redis data and cache), SMTP (mail-smtp), MongoDB (orm-mongodb) and OTLP telemetryaws-lc-rs, as the rustls crypto providerPost-quantum hybrid key exchange (X25519MLKEM768, offered first) and a FIPS-capable build
HMAC, digests, randomness and signatures in rullst-auth, rullst-capital, rullst-core, rullst-labs, rullst-media, rullst-privacy, rullst-studio and rullst-supervisionring, used directlyPortable to wasm and edge targets, where aws-lc-rs is not available

ring is not a rustls provider in Rullst: no Rullst crate enables rustls’ ring feature. JWT signing (rullst-auth jwt) uses jsonwebtoken’s aws_lc_rs backend.

How each client chooses its provider:

  • SQLx and MongoDB select aws-lc-rs at compile time (tls-rustls-aws-lc-rs, rustls-tls-aws-lc). SQLx switches to ring as soon as any crate in the application enables one of its ring TLS features (tls-rustls, tls-rustls-ring and the tls-rustls-ring-* variants), so an application’s own sqlx dependency should use tls-rustls-aws-lc-rs, as generated projects do.
  • reqwest (and therefore the OTLP exporter) and lettre use the rustls process default when one is installed, otherwise aws-lc-rs.
  • Redis uses only the process default. When none is installed and the application’s dependencies enable both ring and aws-lc-rs, rustls cannot choose and panics, so Rullst installs aws-lc-rs before a rediss:// connection.

Rullst installs aws-lc-rs as the rustls process default when none is installed: at Server start-up, before telemetry and the database pool, when Core’s orm, storage-s3 or telemetry feature is enabled (the Core features that build rustls clients); when the OTLP exporter builds its HTTP client; before a Redis TLS connection; and in the standalone rullst-studio binary. It never replaces an installed provider, so an application chooses its own by installing it first, at the top of main, before Server::run and before it builds any client:

#[rullst::runtime::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // With `rustls = { version = "0.23", features = ["fips"] }`; any rustls
    // `CryptoProvider` works the same way.
    rustls::crypto::default_fips_provider()
        .install_default()
        .map_err(|_| "a rustls provider was already installed")?;
    // Build clients and start the `Server` here.
    Ok(())
}

Rullst does not enable rustls’ fips feature (it needs CMake and Go to build the AWS-LC FIPS module) and makes no FIPS validation claim. An application that adds a ring rustls feature of its own can keep aws-lc-rs as the default this way; SQLx then still uses ring, as described above.

Audit chains

Audit records use an unambiguous canonical representation and an HMAC key of at least 32 bytes (MIN_AUDIT_KEY_BYTES) with at least eight distinct byte values. Verification must cover the ordered sequence, not just isolated records. A valid chain is tamper-evident; it cannot stop an attacker who can delete every record or steal the key. Store the log and key in separate protected systems. A new AuditChain starts at sequence 1 from the genesis predecessor, so a restarted writer must continue its persisted trail with AuditChain::try_resume (added in v13), passing the newest persisted record; that record’s HMAC must verify. Exactly one writer may own a persisted chain, and deleting the newest records before a restart is detected only against an external checkpoint of the last sequence and hash.

Supply-chain and compliance evidence

Repository workflows can run dependency policy, RustSec, CodeQL, fuzzing, sanitizers, SemVer checks, SBOM generation, and release attestations. Their scope and blocking status are defined by the workflow files. Informational checks must not be described as formal gates.

Neither those workflows nor this crate certify an application for SOC 2, ISO 27001, PCI DSS, FedRAMP, or any OWASP level. See AUDIT.md and the external audit kit for reproduction and evidence requirements. cargo rullst audit --compliance writes a bounded SECURITY_COMPLIANCE.md evidence report for one application; it is generated, not kept in the repository, and is not a certification.

Deployment checklist

  • Mount middleware in a tested order and use exact webhook CSRF exemptions.
  • Configure secure session, webhook, audit, and encryption keys; reject weak or empty live credentials.
  • Define trusted proxies with Server::trusted_proxies (or [security] trusted_proxies) listing only the real proxy networks, and use the direct peer as the default identity.
  • Apply owner/tenant/role checks server-side for every data operation.
  • Validate per-response CSP nonces against rendered pages.
  • Export telemetry and audit records to durable, access-controlled storage.
  • Run negative integration tests and an independent security review for the application’s actual configuration.