acme_proxy_admin/webadmin/
filter.rs1use std::net::SocketAddr;
33use std::sync::Arc;
34
35use axum::body::Body;
36use axum::extract::{ConnectInfo, State};
37use axum::http::Request;
38use axum::middleware::Next;
39use axum::response::{IntoResponse, Response};
40use tracing::{Span, field, warn};
41
42use acme_proxy_core::client::{ClientIp, ProxyPolicy};
43use acme_proxy_core::config::Config;
44use acme_proxy_policy::filter::{ConnectionContext, Effect, FilterPolicy, Outcome, Stage};
45
46use super::error::AdminError;
47use super::pages::PageError;
48
49const IDENTIFIER_ONLY_TYPES: &[&str] = &["identifiers", "eab", "ipam"];
51
52pub fn build(config: &Config) -> anyhow::Result<Arc<FilterPolicy>> {
61 let filter = &config.admin.filter;
62 let prefix = |error: anyhow::Error| anyhow::anyhow!("admin.filter: {error}");
63
64 if filter.rules.is_empty() {
65 if let Some(name) = filter.rule.keys().next() {
66 anyhow::bail!(
67 "[admin.filter.rule.{name}] is configured but admin.filter.rules is empty; \
68 list the rules to evaluate, in order"
69 );
70 }
71 let proxy =
72 ProxyPolicy::new(&filter.trusted_proxies, &filter.forwarded_header).map_err(prefix)?;
73 return Ok(Arc::new(FilterPolicy::new(
74 Vec::new(),
75 Vec::new(),
76 Effect::Allow,
77 proxy,
78 )));
79 }
80
81 for (name, check) in &filter.check {
82 let kind = check.r#type.trim();
83 if IDENTIFIER_ONLY_TYPES.contains(&kind) {
84 anyhow::bail!(
85 "admin.filter.check.{name} is type = \"{kind}\", which decides about the \
86 names or account of an ACME order; the admin listener never sees one, so it \
87 could not decide anything. Use allowed_ip, path, reverse_dns or custom"
88 );
89 }
90 }
91
92 let policy = acme_proxy_policy::filter::build::build(filter, &config.dns, None, false)
93 .map_err(prefix)?;
94
95 if let Some(rule) = policy
96 .rules()
97 .iter()
98 .find(|rule| !rule.stages.contains(Stage::Connection))
99 {
100 anyhow::bail!(
101 "admin.filter.rule.{} is evaluated at the identifier stage only, which the admin \
102 listener never reaches, so it would never run; drop the stages override on its \
103 checks or drop the rule",
104 rule.name
105 );
106 }
107
108 Ok(Arc::new(policy))
109}
110
111pub async fn admin_filter_middleware(
113 State(policy): State<Arc<FilterPolicy>>,
114 mut request: Request<Body>,
115 next: Next,
116) -> Response {
117 let peer = request
118 .extensions()
119 .get::<ConnectInfo<SocketAddr>>()
120 .map(|ConnectInfo(addr)| addr.ip());
121
122 let client_ip = policy.proxy().resolve(peer, request.headers());
123 request.extensions_mut().insert(ClientIp(client_ip));
124 if let Some(ip) = client_ip {
125 Span::current().record("client_ip", field::display(ip));
126 }
127
128 let path = request.uri().path().to_string();
129 let context = ConnectionContext {
130 client_ip,
131 method: request.method(),
132 path: &path,
133 };
134
135 match policy.check_connection(&context).await {
136 Outcome::Allow => next.run(request).await,
137 outcome => refusal(&outcome, client_ip, &path),
138 }
139}
140
141fn refusal(outcome: &Outcome, client_ip: Option<std::net::IpAddr>, path: &str) -> Response {
147 let error = match outcome {
148 Outcome::Deny(detail) => {
149 warn!(event = "filter_request_blocked", outcome = "failure", listener = "admin", client_ip = ?client_ip, path, %detail);
150 AdminError::access_denied(detail.clone())
151 }
152 Outcome::Undecided(_) | Outcome::Allow => AdminError::internal(),
153 };
154
155 if path == "/api" || path.starts_with("/api/") {
156 error.into_response()
157 } else {
158 PageError::from(error).into_response()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use axum::http::{StatusCode, header};
166 use axum::{Router, middleware, routing::get};
167 use tower::ServiceExt;
168
169 use acme_proxy_core::config::{CheckConfig, RuleConfig};
170
171 fn config_with(check: CheckConfig) -> Config {
172 let mut config = Config::default();
173 let filter = &mut config.admin.filter;
174 filter.rules = vec!["mgmt".to_string()];
175 filter.check.insert("net".to_string(), check);
176 filter.rule.insert(
177 "mgmt".to_string(),
178 RuleConfig {
179 when: "net".to_string(),
180 then: "allow".to_string(),
181 ..RuleConfig::default()
182 },
183 );
184 config
185 }
186
187 fn allowed_ip(allow: &[&str]) -> CheckConfig {
188 CheckConfig {
189 r#type: "allowed_ip".to_string(),
190 allow: allow.iter().map(ToString::to_string).collect(),
191 ..CheckConfig::default()
192 }
193 }
194
195 fn app(config: &Config) -> Router {
196 let policy = build(config).expect("policy must build");
197 Router::new()
198 .route("/api/x", get(|| async { "ok" }))
199 .route("/ui/x", get(|| async { "ok" }))
200 .route(
201 "/ip",
202 get(|parts: axum::http::request::Parts| async move {
203 format!("{:?}", parts.extensions.get::<ClientIp>())
204 }),
205 )
206 .layer(middleware::from_fn_with_state(
207 policy,
208 admin_filter_middleware,
209 ))
210 }
211
212 fn request(uri: &str, peer: [u8; 4], forwarded: Option<&str>) -> Request<Body> {
213 let mut builder = Request::builder().uri(uri);
214 if let Some(value) = forwarded {
215 builder = builder.header("x-forwarded-for", value);
216 }
217 let mut request = builder.body(Body::empty()).unwrap();
218 request
219 .extensions_mut()
220 .insert(ConnectInfo(SocketAddr::from((peer, 4711))));
221 request
222 }
223
224 async fn body(response: Response) -> String {
225 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
226 .await
227 .unwrap();
228 String::from_utf8(bytes.to_vec()).unwrap()
229 }
230
231 #[tokio::test]
232 async fn no_rules_serves_everyone() {
233 let app = app(&Config::default());
234 let response = app
235 .oneshot(request("/api/x", [203, 0, 113, 5], None))
236 .await
237 .unwrap();
238 assert_eq!(response.status(), StatusCode::OK);
239 }
240
241 #[tokio::test]
242 async fn an_allowed_address_is_served() {
243 let app = app(&config_with(allowed_ip(&["10.0.0.0/8"])));
244 let response = app
245 .oneshot(request("/api/x", [10, 1, 2, 3], None))
246 .await
247 .unwrap();
248 assert_eq!(response.status(), StatusCode::OK);
249 }
250
251 #[tokio::test]
252 async fn a_refusal_under_api_is_json() {
253 let app = app(&config_with(allowed_ip(&["10.0.0.0/8"])));
254 let response = app
255 .oneshot(request("/api/x", [203, 0, 113, 5], None))
256 .await
257 .unwrap();
258 assert_eq!(response.status(), StatusCode::FORBIDDEN);
259 let json: serde_json::Value = serde_json::from_str(&body(response).await).unwrap();
260 assert_eq!(json["error"], "access_denied");
261 }
262
263 #[tokio::test]
264 async fn a_refusal_elsewhere_is_an_html_page() {
265 let app = app(&config_with(allowed_ip(&["10.0.0.0/8"])));
266 let response = app
267 .oneshot(request("/ui/x", [203, 0, 113, 5], None))
268 .await
269 .unwrap();
270 assert_eq!(response.status(), StatusCode::FORBIDDEN);
271 let content_type = response.headers()[header::CONTENT_TYPE]
272 .to_str()
273 .unwrap()
274 .to_string();
275 assert!(content_type.starts_with("text/html"), "{content_type}");
276 assert!(body(response).await.contains("access_denied"));
277 }
278
279 #[tokio::test]
282 async fn a_trusted_proxys_forwarded_client_is_the_one_checked() {
283 let mut config = config_with(allowed_ip(&["198.51.100.0/24"]));
284 config.admin.filter.trusted_proxies = vec!["172.16.0.0/12".to_string()];
285 let app = app(&config);
286
287 let allowed = app
288 .clone()
289 .oneshot(request("/api/x", [172, 18, 0, 2], Some("198.51.100.9")))
290 .await
291 .unwrap();
292 assert_eq!(allowed.status(), StatusCode::OK);
293
294 let refused = app
295 .oneshot(request("/api/x", [172, 18, 0, 2], Some("203.0.113.5")))
296 .await
297 .unwrap();
298 assert_eq!(refused.status(), StatusCode::FORBIDDEN);
299 }
300
301 #[tokio::test]
302 async fn an_untrusted_peers_forwarded_header_is_ignored() {
303 let app = app(&config_with(allowed_ip(&["198.51.100.0/24"])));
304 let response = app
305 .oneshot(request("/api/x", [203, 0, 113, 5], Some("198.51.100.9")))
306 .await
307 .unwrap();
308 assert_eq!(response.status(), StatusCode::FORBIDDEN);
309 }
310
311 #[tokio::test]
314 async fn the_client_address_is_recorded_without_any_rule() {
315 let mut config = Config::default();
316 config.admin.filter.trusted_proxies = vec!["172.16.0.0/12".to_string()];
317 let response = app(&config)
318 .oneshot(request("/ip", [172, 18, 0, 2], Some("198.51.100.9")))
319 .await
320 .unwrap();
321 assert!(body(response).await.contains("198.51.100.9"));
322 }
323
324 #[test]
325 fn an_unknown_is_a_500() {
326 let response = refusal(
327 &Outcome::Undecided("script died".to_string()),
328 None,
329 "/api/x",
330 );
331 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
332 }
333
334 #[test]
335 fn identifier_only_check_types_are_refused_by_name() {
336 for kind in IDENTIFIER_ONLY_TYPES {
337 let error = build(&config_with(CheckConfig {
338 r#type: (*kind).to_string(),
339 ..CheckConfig::default()
340 }))
341 .unwrap_err()
342 .to_string();
343 assert!(error.contains("admin.filter.check.net"), "{error}");
344 assert!(error.contains(kind), "{error}");
345 }
346 }
347
348 #[test]
349 fn a_rule_moved_to_the_identifier_stage_is_refused() {
350 let mut check = allowed_ip(&["10.0.0.0/8"]);
351 check.stages = vec!["identifiers".to_string()];
352 let error = build(&config_with(check)).unwrap_err().to_string();
353 assert!(error.contains("admin.filter.rule.mgmt"), "{error}");
354 }
355
356 #[test]
357 fn an_engine_error_names_the_section() {
358 let error = build(&config_with(allowed_ip(&["not-a-network"])))
359 .unwrap_err()
360 .to_string();
361 assert!(error.starts_with("admin.filter: "), "{error}");
362 }
363
364 #[test]
365 fn a_rule_without_rules_is_refused() {
366 let mut config = config_with(allowed_ip(&["10.0.0.0/8"]));
367 config.admin.filter.rules.clear();
368 let error = build(&config).unwrap_err().to_string();
369 assert!(error.contains("admin.filter.rules is empty"), "{error}");
370 }
371
372 #[test]
373 fn a_bad_trusted_proxy_is_refused_without_rules() {
374 let mut config = Config::default();
375 config.admin.filter.trusted_proxies = vec!["nope".to_string()];
376 assert!(build(&config).is_err());
377 }
378}