Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,16 @@ jwt_auth_fail_timeout_seconds = 300
# HTTP header to use to determine the real client IP, if the Signer is behind a proxy (e.g. nginx)
# OPTIONAL. If missing, the client IP will be taken directly from the TCP connection.
# [signer.reverse_proxy]
# 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.
# unique = "X-Real-IP"
# 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.
# rightmost = "X-Forwarded-For"
# Type of reverse proxy configuration. Supported values:
# - unique: use a single HTTP header value as the client IP.
# - rightmost: use the rightmost IP from a comma-separated list of IPs in the HTTP header.
# type = "unique"
# Unique: HTTP header name to use to determine the real client IP. If the header appears multiple times, the request will be rejected.
# header = "X-Real-IP"
# 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.
# header = "X-Forwarded-For"
# 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.
# trusted_count = 1

# [signer.tls_mode]
# How to use TLS for the Signer's HTTP server; two modes are supported:
Expand Down
33 changes: 30 additions & 3 deletions crates/common/src/config/signer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::{
collections::HashMap,
fmt::Display,
net::{Ipv4Addr, SocketAddr},
num::NonZeroUsize,
path::PathBuf,
};

Expand Down Expand Up @@ -71,12 +73,37 @@ pub enum TlsMode {

/// Reverse proxy setup, used to extract real client's IP
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ReverseProxyHeaderSetup {
#[default]
None,
Unique(String),
Rightmost(String),
Unique {
header: String,
},
Rightmost {
header: String,
trusted_count: NonZeroUsize,
},
}

impl Display for ReverseProxyHeaderSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReverseProxyHeaderSetup::None => write!(f, "None"),
ReverseProxyHeaderSetup::Unique { header } => {
write!(f, "\"{header} (unique)\"")
}
ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => {
let suffix = match trusted_count.get() % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
};
write!(f, "\"{header} ({trusted_count}{suffix} from the right)\"")
}
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down
2 changes: 1 addition & 1 deletion crates/signer/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ impl SigningService {
loaded_proxies,
jwt_auth_fail_limit =? state.jwt_auth_fail_limit,
jwt_auth_fail_timeout =? state.jwt_auth_fail_timeout,
reverse_proxy =? state.reverse_proxy,
reverse_proxy =% state.reverse_proxy,
"Starting signing service"
);

Expand Down
25 changes: 19 additions & 6 deletions crates/signer/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum IpError {
InvalidValue,
#[error("header `{0}` appears multiple times but expected to be unique")]
NotUnique(String),
#[error("header does not contain enough values: found {found}, required {required}")]
NotEnoughValues { found: usize, required: usize },
}

/// Get the true client IP from the request headers or fallback to the socket
Expand All @@ -24,8 +26,10 @@ pub fn get_true_ip(
) -> Result<IpAddr, IpError> {
match reverse_proxy {
ReverseProxyHeaderSetup::None => Ok(addr.ip()),
ReverseProxyHeaderSetup::Unique(header) => get_ip_from_unique_header(headers, header),
ReverseProxyHeaderSetup::Rightmost(header) => get_ip_from_rightmost_value(headers, header),
ReverseProxyHeaderSetup::Unique { header } => get_ip_from_unique_header(headers, header),
ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => {
get_ip_from_rightmost_value(headers, header, trusted_count.get())
}
}
}

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

fn get_ip_from_rightmost_value(headers: &HeaderMap, header_name: &str) -> Result<IpAddr, IpError> {
fn get_ip_from_rightmost_value(
headers: &HeaderMap,
header_name: &str,
trusted_count: usize,
) -> Result<IpAddr, IpError> {
let last_value = headers
.get_all(header_name)
.iter()
Expand All @@ -55,10 +63,15 @@ fn get_ip_from_rightmost_value(headers: &HeaderMap, header_name: &str) -> Result
.to_str()
.map_err(|_| IpError::HasInvalidCharacters)?;

// Selecting the first untrusted IP from the right according to:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#selecting_an_ip_address
last_value
.rsplit_once(",")
.map(|(_, rightmost)| rightmost)
.unwrap_or(last_value)
.rsplit(",")
.nth(trusted_count - 1)
.ok_or(IpError::NotEnoughValues {
found: last_value.split(",").count(),
required: trusted_count,
})?
.parse::<IpAddr>()
.map_err(|_| IpError::InvalidValue)
}
13 changes: 9 additions & 4 deletions docs/docs/get_started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,21 +401,26 @@ jwt_auth_fail_timeout_seconds = 300 # The time window in seconds

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:

- `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.
- `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`.
- 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.
- `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`.

Examples:

```toml
[signer.reverse_proxy]
unique = "X-Real-IP"
type = "unique"
header = "X-Real-IP"
```

```toml
[signer.reverse_proxy]
rightmost = "X-Forwarded-For"
type = "rightmost"
header = "X-Forwarded-For"
trusted_count = 1
```

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.

## Custom module

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