Skip to main content

rune_axum_stack/
lib.rs

1//! All-in-one security middleware stack for Axum.
2//!
3//! Composes up to six [`tower::Layer`]s from the `rune-axum-*` family into a single
4//! [`SecurityStack::apply`] call: security headers, CSRF protection, rate limiting, IP
5//! filtering, HTTPS redirect, and body size limiting. Every layer is individually
6//! configurable or removable via builder methods.
7//!
8//! Re-exports the configuration types from each component crate — you only need
9//! `rune-axum-stack` in `[dependencies]`.
10//!
11//! # Default stack
12//!
13//! [`SecurityStack::default()`] enables five layers with safe defaults:
14//!
15//! | Layer | Default |
16//! |-------|---------|
17//! | [`HelmetLayer`] | Standard security headers (no HSTS/CSP — opt-in) |
18//! | [`RedirectHttpsLayer`] | 308 permanent redirect for HTTP requests |
19//! | [`RateLimitLayer`] | 100 requests per 60 s keyed by `X-Forwarded-For` |
20//! | [`BodyLimitLayer`] | 1 MiB `Content-Length` limit |
21//! | [`CsrfLayer`] | Double-submit cookie on POST / PUT / PATCH / DELETE |
22//!
23//! IP filtering ([`IpFilterLayer`]) is **not** included by default — it requires explicit
24//! CIDR configuration via [`.ipfilter()`][SecurityStack::ipfilter].
25//!
26//! # Features
27//!
28//! - Six `rune-axum-*` layers composed into one [`SecurityStack::apply`] call.
29//! - Every layer is individually configurable or removable — use only what you need.
30//! - Outermost-first ordering: security headers wrap all rejection responses (rate-limit
31//!   `429`, CSRF `403`, IP-filter `403`, etc.).
32//! - Re-exports all config types — one crate in `[dependencies]` is enough.
33//! - Works with any Axum router state (`Router<S>`).
34//!
35//! # Quick Start
36//!
37//! ```rust,no_run
38//! use axum::{routing::get, Router};
39//! use rune_axum_stack::SecurityStack;
40//!
41//! let app: Router = SecurityStack::default().apply(
42//!     Router::new().route("/", get(|| async { "ok" })),
43//! );
44//! ```
45//!
46//! # Custom Configuration
47//!
48//! ```rust,no_run
49//! use std::time::Duration;
50//! use axum::{routing::get, Router};
51//! use rune_axum_stack::{
52//!     FilterMode, Helmet, IpFilterConfig, RateLimitConfig, SecurityStack, XFrameOptions,
53//! };
54//!
55//! let app: Router = SecurityStack::new()
56//!     .helmet(Helmet::new().frame_options(XFrameOptions::Deny))
57//!     .ratelimit(RateLimitConfig::new().requests(200).window(Duration::from_secs(30)))
58//!     .ipfilter(IpFilterConfig::new().mode(FilterMode::Blocklist).cidr("10.0.0.0/8"))
59//!     .without_csrf()
60//!     .apply(Router::new().route("/", get(|| async { "ok" })));
61//! ```
62
63pub use rune_axum_csrf::{CsrfConfig, CsrfLayer, CsrfService};
64pub use rune_axum_helmet::{
65    CrossOriginEmbedderPolicy, CrossOriginOpenerPolicy, CrossOriginResourcePolicy, Helmet,
66    HelmetLayer, Hsts, ReferrerPolicy, XFrameOptions,
67};
68pub use rune_axum_ipfilter::{FilterMode, IpFilterConfig, IpFilterLayer, IpFilterService, IpSource};
69pub use rune_axum_ratelimit::{KeyExtractor, RateLimitConfig, RateLimitLayer, RateLimitService};
70pub use rune_axum_redirect_https::{RedirectHttps, RedirectHttpsLayer};
71pub use rune_axum_size::{BodyLimit, BodyLimitLayer, BodyLimitService};
72
73/// Composable security middleware stack for Axum.
74///
75/// Holds an optional configuration for each supported layer. Layers set to `None` are
76/// skipped in [`apply`][SecurityStack::apply]. Start from [`SecurityStack::default()`]
77/// for safe production defaults, or [`SecurityStack::new()`] for the same starting point
78/// and chain builder methods to customise or disable individual layers.
79///
80/// # Layer application order
81///
82/// Layers are applied outermost-first in the following order, so each layer's rejection
83/// response is still wrapped by the layers before it (e.g. security headers appear on
84/// rate-limit rejections):
85///
86/// `helmet` → `redirect_https` → `ipfilter` → `ratelimit` → `body_limit` → `csrf` → handler
87///
88/// # Examples
89///
90/// ```rust,no_run
91/// use axum::{routing::get, Router};
92/// use rune_axum_stack::SecurityStack;
93///
94/// // Five-layer default stack
95/// let app: Router = SecurityStack::default().apply(
96///     Router::new().route("/api", get(|| async { "ok" })),
97/// );
98/// ```
99///
100/// ```rust,no_run
101/// use axum::{routing::post, Router};
102/// use rune_axum_stack::SecurityStack;
103///
104/// // REST API: no CSRF, no HTTPS redirect (TLS terminated upstream)
105/// let app: Router = SecurityStack::new()
106///     .without_csrf()
107///     .without_redirect_https()
108///     .apply(Router::new().route("/api/data", post(|| async { "ok" })));
109/// ```
110#[derive(Clone, Debug)]
111pub struct SecurityStack {
112    helmet: Option<Helmet>,
113    csrf: Option<CsrfConfig>,
114    ratelimit: Option<RateLimitConfig>,
115    ipfilter: Option<IpFilterConfig>,
116    redirect_https: Option<RedirectHttps>,
117    body_limit: Option<BodyLimit>,
118}
119
120impl Default for SecurityStack {
121    fn default() -> Self {
122        Self {
123            helmet: Some(Helmet::new()),
124            csrf: Some(CsrfConfig::new()),
125            ratelimit: Some(RateLimitConfig::new()),
126            ipfilter: None,
127            redirect_https: Some(RedirectHttps::new()),
128            body_limit: Some(BodyLimit::new()),
129        }
130    }
131}
132
133impl SecurityStack {
134    /// Creates a `SecurityStack` with the same safe defaults as [`Default`].
135    ///
136    /// # Examples
137    ///
138    /// ```rust,no_run
139    /// use axum::{routing::get, Router};
140    /// use rune_axum_stack::SecurityStack;
141    ///
142    /// let app: Router = SecurityStack::new()
143    ///     .without_csrf()
144    ///     .apply(Router::new().route("/", get(|| async { "ok" })));
145    /// ```
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Replaces the [`HelmetLayer`] configuration.
151    pub fn helmet(mut self, config: Helmet) -> Self {
152        self.helmet = Some(config);
153        self
154    }
155
156    /// Removes the [`HelmetLayer`] from the stack.
157    pub fn without_helmet(mut self) -> Self {
158        self.helmet = None;
159        self
160    }
161
162    /// Replaces the [`CsrfLayer`] configuration.
163    pub fn csrf(mut self, config: CsrfConfig) -> Self {
164        self.csrf = Some(config);
165        self
166    }
167
168    /// Removes the [`CsrfLayer`] from the stack.
169    ///
170    /// Useful for stateless APIs that authenticate via bearer tokens or API keys
171    /// where traditional CSRF protection is not applicable.
172    pub fn without_csrf(mut self) -> Self {
173        self.csrf = None;
174        self
175    }
176
177    /// Replaces the [`RateLimitLayer`] configuration.
178    pub fn ratelimit(mut self, config: RateLimitConfig) -> Self {
179        self.ratelimit = Some(config);
180        self
181    }
182
183    /// Removes the [`RateLimitLayer`] from the stack.
184    pub fn without_ratelimit(mut self) -> Self {
185        self.ratelimit = None;
186        self
187    }
188
189    /// Enables IP filtering with the given [`IpFilterConfig`].
190    ///
191    /// IP filtering is disabled by default; call this to opt in.
192    pub fn ipfilter(mut self, config: IpFilterConfig) -> Self {
193        self.ipfilter = Some(config);
194        self
195    }
196
197    /// Removes the [`IpFilterLayer`] from the stack (the default state).
198    pub fn without_ipfilter(mut self) -> Self {
199        self.ipfilter = None;
200        self
201    }
202
203    /// Replaces the [`RedirectHttpsLayer`] configuration.
204    pub fn redirect_https(mut self, config: RedirectHttps) -> Self {
205        self.redirect_https = Some(config);
206        self
207    }
208
209    /// Removes the [`RedirectHttpsLayer`] from the stack.
210    ///
211    /// Suitable when TLS is terminated by an upstream proxy that never forwards
212    /// plain HTTP to the application.
213    pub fn without_redirect_https(mut self) -> Self {
214        self.redirect_https = None;
215        self
216    }
217
218    /// Replaces the [`BodyLimitLayer`] configuration.
219    pub fn body_limit(mut self, config: BodyLimit) -> Self {
220        self.body_limit = Some(config);
221        self
222    }
223
224    /// Removes the [`BodyLimitLayer`] from the stack.
225    pub fn without_body_limit(mut self) -> Self {
226        self.body_limit = None;
227        self
228    }
229
230    /// Applies all enabled layers to `router` and returns the wrapped router.
231    ///
232    /// Layers are composed outermost-first:
233    /// `helmet` → `redirect_https` → `ipfilter` → `ratelimit` → `body_limit` → `csrf` → handler.
234    ///
235    /// # Examples
236    ///
237    /// ```rust,no_run
238    /// use axum::{routing::get, Router};
239    /// use rune_axum_stack::SecurityStack;
240    ///
241    /// let app: Router = SecurityStack::default().apply(
242    ///     Router::new().route("/", get(|| async { "ok" })),
243    /// );
244    /// ```
245    pub fn apply<S>(self, router: axum::Router<S>) -> axum::Router<S>
246    where
247        S: Clone + Send + Sync + 'static,
248    {
249        let mut r = router;
250
251        // Applied first → innermost → last to process a request.
252        if let Some(config) = self.csrf {
253            r = r.layer(CsrfLayer::new(config));
254        }
255        if let Some(config) = self.body_limit {
256            r = r.layer(BodyLimitLayer::new(config));
257        }
258        if let Some(config) = self.ratelimit {
259            r = r.layer(RateLimitLayer::new(config));
260        }
261        if let Some(config) = self.ipfilter {
262            r = r.layer(IpFilterLayer::new(config));
263        }
264        if let Some(config) = self.redirect_https {
265            r = r.layer(RedirectHttpsLayer::new(config));
266        }
267        // Applied last → outermost → first to process a request.
268        if let Some(config) = self.helmet {
269            r = r.layer(HelmetLayer::new(config));
270        }
271
272        r
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use axum::{
280        body::Body,
281        routing::{get, post},
282        Router,
283    };
284    use http::StatusCode;
285    use tower::ServiceExt;
286
287    fn base_router() -> Router {
288        Router::new()
289            .route("/", get(|| async { "ok" }))
290            .route("/submit", post(|| async { "ok" }))
291    }
292
293    async fn send(app: Router, req: http::Request<Body>) -> http::Response<Body> {
294        app.oneshot(req).await.unwrap()
295    }
296
297    fn get_req() -> http::Request<Body> {
298        http::Request::builder()
299            .method("GET")
300            .uri("/")
301            .header("x-forwarded-for", "1.2.3.4")
302            .body(Body::empty())
303            .unwrap()
304    }
305
306    fn post_req() -> http::Request<Body> {
307        http::Request::builder()
308            .method("POST")
309            .uri("/submit")
310            .header("x-forwarded-for", "1.2.3.4")
311            .body(Body::empty())
312            .unwrap()
313    }
314
315    #[tokio::test]
316    async fn default_stack_allows_get() {
317        let app = SecurityStack::default().apply(base_router());
318        assert_eq!(send(app, get_req()).await.status(), StatusCode::OK);
319    }
320
321    #[tokio::test]
322    async fn default_stack_sets_security_headers() {
323        let app = SecurityStack::default().apply(base_router());
324        let resp = send(app, get_req()).await;
325        assert_eq!(resp.status(), StatusCode::OK);
326        assert!(resp.headers().contains_key("x-content-type-options"));
327        assert!(resp.headers().contains_key("x-frame-options"));
328    }
329
330    #[tokio::test]
331    async fn default_stack_blocks_post_without_csrf_token() {
332        let app = SecurityStack::default().apply(base_router());
333        assert_eq!(send(app, post_req()).await.status(), StatusCode::FORBIDDEN);
334    }
335
336    #[tokio::test]
337    async fn without_csrf_allows_post() {
338        let app = SecurityStack::default().without_csrf().apply(base_router());
339        assert_eq!(send(app, post_req()).await.status(), StatusCode::OK);
340    }
341
342    #[tokio::test]
343    async fn custom_ratelimit_zero_blocks_request() {
344        let app = SecurityStack::new()
345            .without_csrf()
346            .without_redirect_https()
347            .ratelimit(RateLimitConfig::new().requests(0))
348            .apply(base_router());
349        assert_eq!(send(app, get_req()).await.status(), StatusCode::TOO_MANY_REQUESTS);
350    }
351
352    #[tokio::test]
353    async fn without_ratelimit_passes() {
354        let app = SecurityStack::new()
355            .without_csrf()
356            .without_redirect_https()
357            .without_ratelimit()
358            .apply(base_router());
359        assert_eq!(send(app, get_req()).await.status(), StatusCode::OK);
360    }
361
362    #[tokio::test]
363    async fn body_limit_rejects_oversized_request() {
364        let app = SecurityStack::new()
365            .without_csrf()
366            .without_redirect_https()
367            .body_limit(BodyLimit::new().limit(10))
368            .apply(base_router());
369        let req = http::Request::builder()
370            .method("POST")
371            .uri("/submit")
372            .header("content-length", "100")
373            .body(Body::empty())
374            .unwrap();
375        assert_eq!(send(app, req).await.status(), StatusCode::PAYLOAD_TOO_LARGE);
376    }
377
378    #[tokio::test]
379    async fn custom_helmet_deny_frame_options() {
380        let app = SecurityStack::new()
381            .without_csrf()
382            .without_redirect_https()
383            .helmet(Helmet::new().frame_options(XFrameOptions::Deny))
384            .apply(base_router());
385        let resp = send(app, get_req()).await;
386        assert_eq!(
387            resp.headers().get("x-frame-options").unwrap().to_str().unwrap(),
388            "DENY"
389        );
390    }
391
392    #[tokio::test]
393    async fn without_helmet_no_security_headers() {
394        let app = SecurityStack::new()
395            .without_csrf()
396            .without_redirect_https()
397            .without_helmet()
398            .apply(base_router());
399        let resp = send(app, get_req()).await;
400        assert!(!resp.headers().contains_key("x-content-type-options"));
401    }
402
403    #[tokio::test]
404    async fn redirect_https_triggers_on_forwarded_proto() {
405        let app = SecurityStack::new()
406            .without_csrf()
407            .apply(base_router());
408        let req = http::Request::builder()
409            .method("GET")
410            .uri("http://example.com/")
411            .header("x-forwarded-proto", "http")
412            .header("host", "example.com")
413            .body(Body::empty())
414            .unwrap();
415        assert_eq!(send(app, req).await.status(), StatusCode::PERMANENT_REDIRECT);
416    }
417
418    #[tokio::test]
419    async fn without_redirect_https_no_redirect() {
420        let app = SecurityStack::new()
421            .without_csrf()
422            .without_redirect_https()
423            .apply(base_router());
424        let req = http::Request::builder()
425            .method("GET")
426            .uri("http://example.com/")
427            .header("x-forwarded-proto", "http")
428            .header("host", "example.com")
429            .body(Body::empty())
430            .unwrap();
431        assert_eq!(send(app, req).await.status(), StatusCode::OK);
432    }
433
434    #[tokio::test]
435    async fn ipfilter_blocks_configured_cidr() {
436        let app = SecurityStack::new()
437            .without_csrf()
438            .without_redirect_https()
439            .ipfilter(
440                IpFilterConfig::new()
441                    .mode(FilterMode::Blocklist)
442                    .cidr("1.2.3.4/32"),
443            )
444            .apply(base_router());
445        assert_eq!(send(app, get_req()).await.status(), StatusCode::FORBIDDEN);
446    }
447
448    #[tokio::test]
449    async fn security_headers_present_on_rate_limit_rejection() {
450        let app = SecurityStack::new()
451            .without_csrf()
452            .without_redirect_https()
453            .ratelimit(RateLimitConfig::new().requests(0))
454            .apply(base_router());
455        let resp = send(app, get_req()).await;
456        assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
457        assert!(resp.headers().contains_key("x-content-type-options"));
458    }
459
460    #[tokio::test]
461    async fn all_layers_disabled_passes_through() {
462        let app = SecurityStack::new()
463            .without_helmet()
464            .without_csrf()
465            .without_ratelimit()
466            .without_ipfilter()
467            .without_redirect_https()
468            .without_body_limit()
469            .apply(base_router());
470        assert_eq!(send(app, get_req()).await.status(), StatusCode::OK);
471    }
472}