Skip to content

Commit 96ecfcf

Browse files
committed
Set a count of trusted proxies for rightmost config
1 parent e58d67e commit 96ecfcf

File tree

5 files changed

+69
-18
lines changed

5 files changed

+69
-18
lines changed

config.example.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,16 @@ jwt_auth_fail_timeout_seconds = 300
188188
# HTTP header to use to determine the real client IP, if the Signer is behind a proxy (e.g. nginx)
189189
# OPTIONAL. If missing, the client IP will be taken directly from the TCP connection.
190190
# [signer.reverse_proxy]
191-
# Unique: HTTP header name to use to determine the real client IP. Expected to appear only once in the request. Requests with multiple values of this header will be rejected.
192-
# unique = "X-Real-IP"
193-
# Rightmost: HTTP header name to use to determine the real client IP from a comma-separated list of IPs. Rightmost IP is the client IP. If the header appears multiple times, the last value will be used.
194-
# rightmost = "X-Forwarded-For"
191+
# Type of reverse proxy configuration. Supported values:
192+
# - unique: use a single HTTP header value as the client IP.
193+
# - rightmost: use the rightmost IP from a comma-separated list of IPs in the HTTP header.
194+
# type = "unique"
195+
# Unique: HTTP header name to use to determine the real client IP. If the header appears multiple times, the request will be rejected.
196+
# header = "X-Real-IP"
197+
# Rightmost: HTTP header name to use to determine the real client IP from a comma-separated list of IPs. If the header appears multiple times, the last value will be used.
198+
# header = "X-Forwarded-For"
199+
# Rightmost: number of trusted proxies in front of the Signer, whose IPs will be skipped when extracting the client IP from the rightmost side of the list. Must be greater than 0.
200+
# trusted_count = 1
195201

196202
# [signer.tls_mode]
197203
# How to use TLS for the Signer's HTTP server; two modes are supported:

crates/common/src/config/signer.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::{
22
collections::HashMap,
3+
fmt::Display,
34
net::{Ipv4Addr, SocketAddr},
5+
num::NonZeroUsize,
46
path::PathBuf,
57
};
68

@@ -71,12 +73,37 @@ pub enum TlsMode {
7173

7274
/// Reverse proxy setup, used to extract real client's IP
7375
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
74-
#[serde(rename_all = "snake_case")]
76+
#[serde(rename_all = "snake_case", tag = "type")]
7577
pub enum ReverseProxyHeaderSetup {
7678
#[default]
7779
None,
78-
Unique(String),
79-
Rightmost(String),
80+
Unique {
81+
header: String,
82+
},
83+
Rightmost {
84+
header: String,
85+
trusted_count: NonZeroUsize,
86+
},
87+
}
88+
89+
impl Display for ReverseProxyHeaderSetup {
90+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91+
match self {
92+
ReverseProxyHeaderSetup::None => write!(f, "None"),
93+
ReverseProxyHeaderSetup::Unique { header } => {
94+
write!(f, "\"{header} (unique)\"")
95+
}
96+
ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => {
97+
let suffix = match trusted_count.get() % 10 {
98+
1 => "st",
99+
2 => "nd",
100+
3 => "rd",
101+
_ => "th",
102+
};
103+
write!(f, "\"{header} ({trusted_count}{suffix} from the right)\"")
104+
}
105+
}
106+
}
80107
}
81108

82109
#[derive(Debug, Serialize, Deserialize, Clone)]

crates/signer/src/service.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl SigningService {
126126
loaded_proxies,
127127
jwt_auth_fail_limit =? state.jwt_auth_fail_limit,
128128
jwt_auth_fail_timeout =? state.jwt_auth_fail_timeout,
129-
reverse_proxy =? state.reverse_proxy,
129+
reverse_proxy =% state.reverse_proxy,
130130
"Starting signing service"
131131
);
132132

crates/signer/src/utils.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub enum IpError {
1313
InvalidValue,
1414
#[error("header `{0}` appears multiple times but expected to be unique")]
1515
NotUnique(String),
16+
#[error("header does not contain enough values: found {found}, required {required}")]
17+
NotEnoughValues { found: usize, required: usize },
1618
}
1719

1820
/// Get the true client IP from the request headers or fallback to the socket
@@ -24,8 +26,10 @@ pub fn get_true_ip(
2426
) -> Result<IpAddr, IpError> {
2527
match reverse_proxy {
2628
ReverseProxyHeaderSetup::None => Ok(addr.ip()),
27-
ReverseProxyHeaderSetup::Unique(header) => get_ip_from_unique_header(headers, header),
28-
ReverseProxyHeaderSetup::Rightmost(header) => get_ip_from_rightmost_value(headers, header),
29+
ReverseProxyHeaderSetup::Unique { header } => get_ip_from_unique_header(headers, header),
30+
ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => {
31+
get_ip_from_rightmost_value(headers, header, trusted_count.get())
32+
}
2933
}
3034
}
3135

@@ -46,7 +50,11 @@ fn get_ip_from_unique_header(headers: &HeaderMap, header_name: &str) -> Result<I
4650
Ok(ip)
4751
}
4852

49-
fn get_ip_from_rightmost_value(headers: &HeaderMap, header_name: &str) -> Result<IpAddr, IpError> {
53+
fn get_ip_from_rightmost_value(
54+
headers: &HeaderMap,
55+
header_name: &str,
56+
trusted_count: usize,
57+
) -> Result<IpAddr, IpError> {
5058
let last_value = headers
5159
.get_all(header_name)
5260
.iter()
@@ -55,10 +63,15 @@ fn get_ip_from_rightmost_value(headers: &HeaderMap, header_name: &str) -> Result
5563
.to_str()
5664
.map_err(|_| IpError::HasInvalidCharacters)?;
5765

66+
// Selecting the first untrusted IP from the right according to:
67+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#selecting_an_ip_address
5868
last_value
59-
.rsplit_once(",")
60-
.map(|(_, rightmost)| rightmost)
61-
.unwrap_or(last_value)
69+
.rsplit(",")
70+
.nth(trusted_count - 1)
71+
.ok_or(IpError::NotEnoughValues {
72+
found: last_value.split(",").count(),
73+
required: trusted_count,
74+
})?
6275
.parse::<IpAddr>()
6376
.map_err(|_| IpError::InvalidValue)
6477
}

docs/docs/get_started/configuration.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -401,21 +401,26 @@ jwt_auth_fail_timeout_seconds = 300 # The time window in seconds
401401

402402
The rate limit is applied to the IP address of the client making the request. By default, the IP is extracted directly from the TCP connection. If you're running the Signer service behind a reverse proxy (e.g. Nginx), you can configure it to extract the IP from a custom HTTP header instead. There're two options:
403403

404-
- `unique`: The name of the HTTP header that contains the IP. This header is expected to appear only once in the request. This is common when using `X-Real-IP`, `True-Client-IP`, etc. If a request is received that has multiple values for this header, it will be considered invalid and rejected.
405-
- `rightmost`: The name of the HTTP header that contains a comma-separated list of IPs. The rightmost IP in the list is used. If the header appears multiple times, the last occurrence is used. This is common when using `X-Forwarded-For`.
404+
- unique: Provides an HTTP header that contains the IP. This header is expected to appear only once in the request. This is common when using `X-Real-IP`, `True-Client-IP`, etc. If a request has multiple values for this header, it will be considered invalid and rejected.
405+
- `rightmost`: Provides an HTTP header that contains a comma-separated list of IPs. The nth rightmost IP in the list is used. If the header appears multiple times, the last occurrence is used. This is common when using `X-Forwarded-For`.
406406

407407
Examples:
408408

409409
```toml
410410
[signer.reverse_proxy]
411-
unique = "X-Real-IP"
411+
type = "unique"
412+
header = "X-Real-IP"
412413
```
413414

414415
```toml
415416
[signer.reverse_proxy]
416-
rightmost = "X-Forwarded-For"
417+
type = "rightmost"
418+
header = "X-Forwarded-For"
419+
trusted_count = 1
417420
```
418421

422+
Note: `trusted_count` is the number of trusted proxies in front of the Signer service, but the last proxy won't add its address, so the number of skipped IPs is `trusted_count - 1`. See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#trusted_proxy_count) for more info.
423+
419424
## Custom module
420425

421426
We currently provide a test module that needs to be built locally. To build the module run:

0 commit comments

Comments
 (0)