Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -758,4 +758,5 @@ public class SecurityConfig {

- [Server Authentication](./10-server-authentication.md) - Configure Spring Security
- [Actuator Security](./20-actuator-security.md) - Secure client endpoints
- [SSRF Protection](./40-ssrf-protection.md) - Block SSRF attacks via the instance registration endpoint
- [Spring Security CSRF Documentation](https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html)
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
sidebar_position: 40
sidebar_custom_props:
icon: 'shield'
---

# SSRF Protection

Spring Boot Admin Server includes opt-in protection against Server-Side Request Forgery (SSRF) attacks that can be
triggered through the instance registration API.

## The Risk

When a Spring Boot application registers with the Admin Server, it sends a `POST /instances` request containing
`healthUrl`, `managementUrl`, and `serviceUrl`. The Admin Server immediately begins making outbound HTTP requests to
those URLs to poll health status and discover actuator endpoints. It also exposes a proxy at
`/instances/{id}/actuator/**` that forwards requests verbatim to the registered management URL.

Without authentication or URL validation on `POST /instances`, an attacker can:

1. Register a fake instance pointing to `http://169.254.169.254/latest/meta-data/` (AWS IMDSv1)
2. The Admin Server polls the metadata endpoint automatically and stores the response
3. The attacker retrieves the response — including IAM credentials — through the actuator proxy

This applies to loopback addresses, RFC 1918 private ranges, and any internal service reachable from the server's
network, not just cloud metadata endpoints.

:::warning
SSRF protection is **disabled by default** to avoid breaking existing deployments where the Admin Server legitimately
communicates with services on private IP ranges. Enable it explicitly and configure an allowlist for any intranet
services that need to register.
:::

---

## Enabling SSRF Protection

Add the following to your `application.yml`:

```yaml
spring:
boot:
admin:
ssrf-protection:
enabled: true
```

When enabled, all URLs submitted during instance registration (`healthUrl`, `managementUrl`, `serviceUrl`) are
validated before the instance is stored. The proxy also re-validates the resolved target URL before each outbound
request.

---

## What Gets Blocked

When protection is enabled, the following are rejected by default:

| Category | Examples |
|---|---|
| Loopback | `localhost`, `127.0.0.1`, `127.x.x.x`, `::1` |
| Link-local (cloud metadata) | `169.254.0.0/16` — includes AWS IMDSv1 (`169.254.169.254`), GCP, Azure |
| RFC 1918 Class A | `10.0.0.0/8` |
| RFC 1918 Class B | `172.16.0.0/12` (172.16.x – 172.31.x) |
| RFC 1918 Class C | `192.168.0.0/16` |
| IPv6 link-local | `fe80::/10` |
| IPv6 unique-local | `fc00::/7` (fc and fd prefixes) |
| IPv4-mapped IPv6 | `::ffff:` prefix embedding a private IPv4 |
| Unspecified address | `0.0.0.0` |
| Disallowed schemes | Anything other than `http` and `https` (e.g. `file://`, `ftp://`) |

Registration attempts targeting any of these return `400 Bad Request`. Proxy requests that resolve to a blocked address
return `403 Forbidden`.

:::note
Hostname-to-IP resolution is **not** performed during validation. Only the literal hostname string from the URL is
checked. An attacker who controls a public DNS record pointing to a private IP (DNS rebinding) is not blocked by this
validator alone. Use IMDSv2 on AWS or network-level egress controls as additional layers of defence.
:::

---

## Allowing Internal Services

If your Admin Server legitimately needs to reach services on private addresses — for example in an on-premises or
Kubernetes cluster deployment — add those hosts to the allowlist. An allowlisted host bypasses all block checks.

### Exact host match

```yaml
spring:
boot:
admin:
ssrf-protection:
enabled: true
allowed-hosts:
- 192.168.1.100
- monitoring-service.internal
```

### Glob-style suffix pattern

Use `*.suffix` to allow an entire subdomain:

```yaml
spring:
boot:
admin:
ssrf-protection:
enabled: true
allowed-hosts:
- "*.svc.cluster.local" # all Kubernetes services
- "*.internal.corp"
```

The glob `*.svc.cluster.local` matches `my-service.svc.cluster.local` but not `svc.cluster.local` itself. Matching
is case-insensitive.

---

## Blocking Additional Hosts

To block hostnames beyond the built-in private ranges — for example internal domains that should never register — add
regex patterns to `blocked-host-patterns`:

```yaml
spring:
boot:
admin:
ssrf-protection:
enabled: true
blocked-host-patterns:
- ".*\\.internal\\.corp$"
- "metadata\\.google\\.internal"
```

Patterns are matched against the raw hostname using `java.util.regex.Pattern`. Invalid patterns are logged as warnings
and skipped.

:::note
The allowlist takes precedence over blocked patterns. A host matching both an `allowed-hosts` entry and a
`blocked-host-patterns` entry is **allowed**.
:::

---

## Allowing Additional Schemes

By default only `http` and `https` are permitted. To add a custom scheme:

```yaml
spring:
boot:
admin:
ssrf-protection:
enabled: true
allowed-schemes:
- http
- https
- grpc
```

---

## Configuration Reference

| Property | Type | Default | Description |
|---|---|---|---|
| `spring.boot.admin.ssrf-protection.enabled` | `boolean` | `false` | Enable SSRF URL validation |
| `spring.boot.admin.ssrf-protection.allowed-schemes` | `Set<String>` | `http, https` | URL schemes that are permitted |
| `spring.boot.admin.ssrf-protection.allowed-hosts` | `List<String>` | _(empty)_ | Hosts exempt from all block checks. Supports exact names and `*.suffix` glob patterns |
| `spring.boot.admin.ssrf-protection.blocked-host-patterns` | `List<String>` | _(empty)_ | Additional Java regex patterns matched against the raw hostname |

---

## Providing a Custom Validator

Override the default `SsrfUrlValidator` bean to implement custom logic — for example, DNS resolution or CIDR matching:

```java
import de.codecentric.boot.admin.server.utils.SsrfUrlValidator;
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CustomSsrfConfig {

@Bean
public SsrfUrlValidator ssrfUrlValidator(AdminServerProperties properties) {
AdminServerProperties.SsrfProtectionProperties ssrfProps =
properties.getSsrfProtection();
// Wrap or extend the default validator
SsrfUrlValidator defaultValidator = new SsrfUrlValidator(ssrfProps);
return url -> {
defaultValidator.validate(url);
// Add custom checks here
};
}
}
```

---

## Dual Validation

SSRF protection runs at two points:

1. **Registration (`POST /instances`)** — `healthUrl`, `managementUrl`, and `serviceUrl` are validated before the
instance is stored. Invalid registrations are rejected with `400 Bad Request`.

2. **Proxy (`/instances/{id}/actuator/**`)** — The resolved target URL is re-validated before each outbound request.
This provides defence-in-depth against scenarios where an endpoint URL is assembled dynamically after registration.
Blocked proxy requests return `403 Forbidden`.

---

## Additional Hardening

SSRF protection alone is not sufficient for a publicly accessible Admin Server. Combine it with:

- **Authentication on `POST /instances`** — The most effective mitigation. See [Server Authentication](./10-server-authentication.md).
- **AWS IMDSv2** — Require a `PUT` request with a TTL header to obtain a metadata token, which the Admin Server cannot
provide without explicit support.
- **Network egress controls** — Firewall rules or security groups that prevent the Admin Server's outbound traffic from
reaching metadata endpoints and internal services.
- **VPC/private network isolation** — Run the Admin Server in a subnet that has no route to sensitive internal services.

---

## See Also

- [Server Authentication](./10-server-authentication.md) - Require authentication before instance registration
- [CSRF Protection](./30-csrf-protection.md) - Protect the registration endpoint against cross-origin forged requests
15 changes: 14 additions & 1 deletion spring-boot-admin-docs/src/site/docs/05-security/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,18 @@ Configure CSRF tokens for Admin UI while allowing client registration:

**See**: [CSRF Protection](./30-csrf-protection.md)

### 4. Mutual TLS (Optional)
### 4. SSRF Protection

Prevent Server-Side Request Forgery via the instance registration API:

- **IP Range Blocking**: Reject private/internal addresses in registered URLs
- **Scheme Allowlist**: Permit only `http` and `https`
- **Allowlist Override**: Explicitly permit intranet services by hostname
- **Proxy-time Validation**: Re-validate resolved URLs before each outbound request

**See**: [SSRF Protection](./40-ssrf-protection.md)

### 5. Mutual TLS (Optional)

Enhanced security with client certificates:

Expand Down Expand Up @@ -144,6 +155,7 @@ Use this checklist to ensure your deployment is secure:
- [ ] Configure form login for UI access
- [ ] Enable HTTP Basic for API/programmatic access
- [ ] Configure CSRF protection with exemptions for `/instances`
- [ ] Enable SSRF protection (`spring.boot.admin.ssrf-protection.enabled=true`)
- [ ] Set up remember-me with secure random key
- [ ] Use HTTPS for deployments
- [ ] Restrict access by IP (if applicable)
Expand Down Expand Up @@ -476,6 +488,7 @@ public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) {
- [Server Authentication](./10-server-authentication.md) - Secure Admin Server with Spring Security
- [Actuator Security](./20-actuator-security.md) - Secure client actuator endpoints
- [CSRF Protection](./30-csrf-protection.md) - Configure CSRF for UI and API
- [SSRF Protection](./40-ssrf-protection.md) - Block SSRF attacks via instance registration

---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2023 the original author or authors.
* Copyright 2014-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -52,6 +52,7 @@
import de.codecentric.boot.admin.server.services.endpoints.ChainingStrategy;
import de.codecentric.boot.admin.server.services.endpoints.ProbeEndpointsStrategy;
import de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy;
import de.codecentric.boot.admin.server.utils.SsrfUrlValidator;
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;

@Configuration(proxyBeanMethods = false)
Expand All @@ -76,11 +77,17 @@ public InstanceFilter instanceFilter() {
return (instance) -> true;
}

@Bean
@ConditionalOnMissingBean
public SsrfUrlValidator ssrfUrlValidator() {
return new SsrfUrlValidator(this.adminServerProperties.getSsrfProtection());
}

@Bean
@ConditionalOnMissingBean
public InstanceRegistry instanceRegistry(InstanceRepository instanceRepository,
InstanceIdGenerator instanceIdGenerator, InstanceFilter instanceFilter) {
return new InstanceRegistry(instanceRepository, instanceIdGenerator, instanceFilter);
InstanceIdGenerator instanceIdGenerator, InstanceFilter instanceFilter, SsrfUrlValidator ssrfUrlValidator) {
return new InstanceRegistry(instanceRepository, instanceIdGenerator, instanceFilter, ssrfUrlValidator);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2023 the original author or authors.
* Copyright 2014-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,8 +18,10 @@

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -49,6 +51,8 @@ public class AdminServerProperties {

private InstanceProxyProperties instanceProxy = new InstanceProxyProperties();

private SsrfProtectionProperties ssrfProtection = new SsrfProtectionProperties();

/**
* The metadata keys which should be sanitized when serializing to JSON
*/
Expand Down Expand Up @@ -203,4 +207,40 @@ public static class InstanceProxyProperties {

}

@lombok.Data
public static class SsrfProtectionProperties {

/**
* Whether SSRF protection is enabled. When enabled, registration URLs are
* validated against blocked schemes and private/internal IP ranges. Default:
* false (opt-in).
*/
private boolean enabled = false;

/**
* URL schemes that are permitted. Any scheme not in this list is blocked.
* Default: http, https.
*/
private Set<String> allowedSchemes = new HashSet<>(asList("http", "https"));

/**
* Hosts (exact match or glob-style suffix patterns) that are explicitly allowed
* even if they would otherwise match a blocked range. Useful for intranet
* deployments where SBA must reach private-IP services.
* <p>
* Example: {@code 192.168.1.100}, {@code *.internal.corp}
*/
private List<String> allowedHosts = new ArrayList<>();

/**
* Additional hostname patterns (regex) to block beyond the built-in private
* ranges. Matched against the raw hostname from the URL (before any DNS
* resolution).
* <p>
* Example: {@code .*\.internal\.corp$}
*/
private List<String> blockedHostPatterns = new ArrayList<>();

}

}
Loading