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.
| Scope | Current boundary |
|---|---|
| Sites and API backends | Bounded 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 instances | The 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 proxy | Verified 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 account | OS/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 availability | Application 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:
- trusted-proxy policy;
- request-body limit;
- request ID;
- tracing;
- secure headers;
- explicit CORS allowlist;
- bounded WAF/RASP;
- CSRF;
- session validation;
- authentication;
- tenant membership resolution;
- role, permission and object-ownership authorization;
- identity/direct-peer rate limit;
- 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 area | Available primitives | Boundary |
|---|---|---|
| Broken access control | RbacGuard, ownership helpers, authenticated user context. | The application must apply a check to every protected object and operation. |
| Cryptographic storage | AES-256-GCM field encryption and zeroizing secret wrappers. | Operators own key generation, storage, rotation, separation, and recovery. |
| Injection | SQLx binds, strict identifier validation, sanitizer and bounded RASP patterns. | Heuristics are not a complete language parser; domain validation and parameterization remain mandatory. |
| Misconfiguration | Nonce-based CSP and a strict HTTP-header baseline. | Proxies and page content change the deployed policy; no scanner grade is guaranteed. |
| Authentication abuse | Login 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 integrity | HMAC 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 leakage | Text-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 risk | Prompt-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 7239Forwarded(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
tracingevent (debugwhen missing, otherwisewarnwith 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 aClientAddrextension (client IP, original peer,via_trusted_proxy), which has no public constructor. trust_forwarded_proto(true)additionally reports the scheme from that trusted peer asClientAddr::forwarded_proto: one unambiguousX-Forwarded-Protovalue, or theproto=of the selectedForwardedelement. Nexus accepts anhttpsreport 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):
| Component | Library | Why |
|---|---|---|
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 telemetry | aws-lc-rs, as the rustls crypto provider | Post-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-supervision | ring, used directly | Portable 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-ringand thetls-rustls-ring-*variants), so an application’s ownsqlxdependency should usetls-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.