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
11 changes: 7 additions & 4 deletions auth/services/oauth/relying_party.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/nuts-foundation/nuts-node/core"
nutsCrypto "github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/didman"
httpclient "github.com/nuts-foundation/nuts-node/http/client"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vdr/didservice"
"github.com/nuts-foundation/nuts-node/vdr/didstore"
Expand Down Expand Up @@ -74,12 +75,14 @@ func (s *relyingParty) RequestAccessToken(ctx context.Context, jwtGrantToken str
if s.secureMode && strings.ToLower(authorizationServerEndpoint.Scheme) != "https" {
return nil, fmt.Errorf("authorization server endpoint must be HTTPS when in strict mode: %s", authorizationServerEndpoint.String())
}
httpClient := &http.Client{}
// The endpoint is resolved from another party's DID document, so use the transport
// with the strict-mode SSRF dial guard.
transport := httpclient.SafeHttpTransport
if s.httpClientTLS != nil {
httpClient.Transport = &http.Transport{
TLSClientConfig: s.httpClientTLS,
}
transport = transport.Clone()
transport.TLSClientConfig = s.httpClientTLS
}
httpClient := &http.Client{Transport: transport}
authClient, err := client.NewHTTPClient("", s.httpClientTimeout, client.WithHTTPClient(httpClient), client.WithRequestEditorFn(core.UserAgentRequestEditor))
if err != nil {
return nil, fmt.Errorf("unable to create HTTP client: %w", err)
Expand Down
15 changes: 15 additions & 0 deletions core/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ func newEmptyTokenGenerator() AuthorizationTokenGenerator {

// NewStrictHTTPClient creates a HTTPRequestDoer that only allows HTTPS calls when strictmode is enabled.
func NewStrictHTTPClient(strictmode bool, client *http.Client) HTTPRequestDoer {
if strictmode && client.CheckRedirect == nil {
// The HTTPS check in Do only guards the first hop, so without this a valid remote
// host could redirect the client onto a plaintext internal endpoint. Setting
// CheckRedirect replaces the standard library's default policy, so the 10-redirect
// cap is reimplemented here.
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
if req.URL.Scheme != "https" {
return errors.New("strictmode is enabled, but redirect target is not over HTTPS")
}
return nil
}
}
return &strictHTTPClient{
client: client,
strictmode: strictmode,
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/deployment/cli-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ The following options apply to the server commands below:
--events.nats.timeout int Timeout for NATS server operations (default 30)
--goldenhammer.enabled Whether to enable automatically fixing DID documents with the required endpoints. (default true)
--goldenhammer.interval duration The interval in which to check for DID documents to fix. (default 10m0s)
--http.client.allowedinternalcidrs strings IP ranges (CIDR notation, e.g. 10.0.0.0/8) exempted from the strict-mode SSRF guard, which otherwise blocks outbound requests to non-public networks. Use to permit internal flows that legitimately target a private address, such as an internal credential offering or an internal OAuth user flow. Leave empty to block all non-public addresses.
--http.client.deniedcidrs strings IP ranges (CIDR notation) that outbound HTTP requests must never target in strict mode, in addition to the built-in blocked ranges (non-public addresses and cloud metadata endpoints). Use for publicly routable ranges that are internal-only in your infrastructure. Takes precedence over http.client.allowedinternalcidrs.
--http.default.address string Address and port the server will be listening to (default ":1323")
--http.default.auth.audience string Expected audience for JWT tokens (default: hostname)
--http.default.auth.authorizedkeyspath string Path to an authorized_keys file for trusted JWT signers
Expand Down
54 changes: 54 additions & 0 deletions docs/pages/deployment/production-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,57 @@ Special attention should be given to:

Since the gRPC interface and ``/n2n`` HTTP endpoint are authenticated using the TLS client certificate,
you can monitor (and potentially deny) access to these endpoint by identifying the exact client.

.. _ssrf-protection:

Outbound HTTP and SSRF protection
*********************************

The Nuts node fetches URLs supplied by other parties, for example OAuth endpoints resolved from
DID documents and OpenID4VCI issuer and wallet endpoints. An attacker could register a URL that
resolves to an address inside your network and let the node request it on their behalf. This is
known as Server-Side Request Forgery (SSRF).

In strict mode the node refuses these outbound HTTP connections to non-public addresses: loopback,
private (RFC 1918), carrier-grade NAT, link-local, unique local, and the other special-purpose ranges
published in the IANA
`IPv4 <https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml>`_ and
`IPv6 <https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml>`_
special-purpose address registries. The check runs at connect time against the resolved IP address, so it
also covers redirects and cannot be bypassed with DNS rebinding. Cloud provider metadata endpoints (such as
``169.254.169.254`` and Azure's ``168.63.129.16``) are always blocked, following the
`OWASP SSRF prevention cheat sheet <https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html>`_.
Redirects that downgrade from HTTPS to HTTP are refused as well.

A blocked request fails with an error like::

strictmode: blocked connection to non-public address 10.0.0.5

Allowing internal ranges
^^^^^^^^^^^^^^^^^^^^^^^^

If a legitimate flow targets a private address, for example an internal OpenID4VCI issuer or an OAuth
flow that stays inside your network, then permit that range with ``http.client.allowedinternalcidrs``.
If part of an allowed range must stay unreachable, then deny that part with ``http.client.deniedcidrs``;
denied ranges take precedence:

.. code-block:: yaml

http:
client:
allowedinternalcidrs:
- 10.0.0.0/8
deniedcidrs:
- 10.5.0.0/16

Keep allowed ranges as narrow as possible. Every address in an allowed range becomes reachable for any
URL an external party can make the node fetch.

Blocking additional ranges
^^^^^^^^^^^^^^^^^^^^^^^^^^

Some networks use publicly routable addresses for internal-only systems. Public addresses are not
covered by the built-in guard, so add such ranges to ``http.client.deniedcidrs`` explicitly.

The built-in blocks for cloud metadata endpoints cannot be lifted with ``allowedinternalcidrs``.
Both options only apply in strict mode; without strict mode the SSRF guard is disabled.
Loading
Loading