From 2ffbc53cec9b0d03040d11109438b2c14a179383 Mon Sep 17 00:00:00 2001 From: Steven van der Vegt Date: Thu, 30 Jul 2026 16:46:06 +0200 Subject: [PATCH 1/2] fix(http): fall back to metadata logging in strictmode http.log=metadata-and-body logs full request and response bodies at Info severity. The loggable content types include application/json and application/x-www-form-urlencoded, which are exactly the OAuth token endpoint's request and response types, so client assertions, VP tokens, authorization codes and issued access tokens all reach the log in full. In strictmode the value is now reset to metadata with a warning at startup, following the existing warn-and-override precedent for unsafe-but-valid configuration values (auth.accesstokenlifespan, cpuprofile). Startup is not failed: remediation for the operator is removing a single config value, and a security patch must be safe to apply without reading release notes. Assisted-by: AI --- docs/pages/deployment/configuration.rst | 1 + docs/pages/deployment/server_options.rst | 144 +++++++++++------------ http/cmd/cmd.go | 2 +- http/engine.go | 7 ++ http/engine_test.go | 46 +++++++- 5 files changed, 126 insertions(+), 74 deletions(-) diff --git a/docs/pages/deployment/configuration.rst b/docs/pages/deployment/configuration.rst index 4385fcb20a..9d6d022d26 100644 --- a/docs/pages/deployment/configuration.rst +++ b/docs/pages/deployment/configuration.rst @@ -93,6 +93,7 @@ As a general safety precaution ``auth.contractvalidators`` ignores the ``dummy`` requesting an access token from another node on ``/n2n/auth/v1/accesstoken`` does not return any error details, ``auth.accesstokenlifespan`` is always 60 seconds, json-ld context can only be downloaded from trusted domains configured in ``jsonld.contexts.remoteallowlist``, +``http.log=metadata-and-body`` is not allowed and is changed to ``metadata`` at startup (request and response bodies on the OAuth endpoints contain credentials, which must not be written to logs), and the ``internalratelimiter`` is always on. Interacting with remote Nuts nodes requires HTTPS: it will refuse to connect to plain HTTP endpoints when in strict mode. \ No newline at end of file diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 3fe005a037..8a3aa6591d 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -2,75 +2,75 @@ :widths: 20 30 50 :class: options-table - ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ===================================================================================================================================================================================================================================================================================================================================================== - Key Default Description - ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ===================================================================================================================================================================================================================================================================================================================================================== - configfile ./config/nuts.yaml Nuts config file - cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. - datadir ./data Directory where the node stores its files. - didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used. - internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. - loggerformat text Log format (text, json) - strictmode true When set, insecure settings are forbidden. - url Public facing URL of the server (required). Must be HTTPS when strictmode is set. - verbosity info Log level (trace, debug, info, warn, error) - httpclient.timeout 30s Request time-out for HTTP clients, such as '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax. - **Auth** - auth.authorizationendpoint.enabled false enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature. - auth.experimental.jwtbearerclient false enables the experimental RFC 7523 jwt-bearer two-VP token request flow. While disabled (the default), requests carrying a service-provider subject identifier are rejected. Subject to change without notice. - **Crypto** - crypto.storage Storage to use, 'fs' for file system (for development purposes), 'vaultkv' for HashiCorp Vault KV store, 'azure-keyvault' for Azure Key Vault, 'external' for an external backend (deprecated). - crypto.azurekv.hsm false Whether to store the key in a hardware security module (HSM). If true, the Azure Key Vault must be configured for HSM usage. Default: false - crypto.azurekv.timeout 10s Timeout of client calls to Azure Key Vault, in Golang time.Duration string format (e.g. 10s). - crypto.azurekv.url The URL of the Azure Key Vault. - crypto.azurekv.auth.type default Credential type to use when authenticating to the Azure Key Vault. Options: default, managed_identity (see https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/azidentity/README.md for an explanation of the options). - crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. - crypto.vault.pathprefix kv The Vault path prefix. - crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). - crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. - **Discovery** - discovery.client.refreshinterval 10m0s Interval at which the client synchronizes with the Discovery Server; refreshing Verifiable Presentations of local DIDs and loading changes, updating the local copy. It only will actually refresh registrations of local DIDs that about to expire (less than 1/4th of their lifetime left). Specified as Golang duration (e.g. 1m, 1h30m). - discovery.definitions.directory ./config/discovery Directory to load Discovery Service Definitions from. If not set, the discovery service will be disabled. If the directory contains JSON files that can't be parsed as service definition, the node will fail to start. - discovery.server.ids [] IDs of the Discovery Service for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. - **HTTP** - http.clientipheader X-Forwarded-For Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs. - http.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). When debug vebosity is set the authorization headers are also logged when the request is fully logged. - http.cache.maxbytes 10485760 HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses. - http.client.allowedinternalcidrs [] 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 [] 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.internal.address 127.0.0.1:8081 Address and port the server will be listening to for internal-facing endpoints. - http.internal.auth.audience Expected audience for JWT tokens (default: hostname) - http.internal.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers - http.internal.auth.type Whether to enable authentication for /internal endpoints, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. - http.public.address \:8080 Address and port the server will be listening to for public-facing endpoints. - **JSONLD** - jsonld.contexts.localmapping [https://nuts.nl/credentials/2024=assets/contexts/nuts-2024.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://w3id.org/vc/status-list/2021/v1=assets/contexts/w3c-statuslist2021.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. - jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json,https://w3id.org/vc/status-list/2021/v1] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. - **PKI** - pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail - pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true - **Storage** - storage.debug false When true, enables extra logging of storage-layer problems (e.g. performance issues). - storage.session.memcached.address [] List of Memcached server addresses. These can be a simple 'host:port' or a Memcached connection URL with scheme, auth and other options. - storage.session.redis.address Redis session database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. If not set it, defaults to an in-memory database. - storage.session.redis.database Redis session database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. - storage.session.redis.password Redis session database password. If set, it overrides the username in the connection URL. - storage.session.redis.username Redis session database username. If set, it overrides the username in the connection URL. - storage.session.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. - storage.session.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. - storage.session.redis.sentinel.password Password for authenticating to Redis Sentinels. - storage.session.redis.sentinel.username Username for authenticating to Redis Sentinels. - storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). - storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL'). - storage.sql.rdsiam.dbuser Database username for IAM authentication. If not specified, the username from the connection string will be used. The database user must be created with IAM authentication enabled. - storage.sql.rdsiam.enabled false Enable AWS RDS IAM authentication for the SQL database connection. When enabled, the node will use temporary IAM tokens instead of passwords. Requires the connection string to be a PostgreSQL or MySQL RDS endpoint without a password. - storage.sql.rdsiam.region AWS region where the RDS instance is located (e.g., 'us-east-1). Required when RDS IAM authentication is enabled. - storage.sql.rdsiam.tokenrefreshinterval 14m0s Interval at which to refresh the IAM authentication token. RDS tokens are valid for 15 minutes, so set this to ensure tokens are refreshed before expiry. Specified as Golang duration (e.g. 10m, 1h). - **Tracing** - tracing.endpoint OTLP collector endpoint for OpenTelemetry tracing (e.g., 'localhost:4318'). When empty, tracing is disabled. - tracing.insecure false Disable TLS for the OTLP connection. - tracing.servicename Service name reported to the tracing backend. Defaults to 'nuts-node'. - **policy** - policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. - policy.authzen.endpoint Base URL of the AuthZen PDP endpoint. Required when any credential profile uses scope_policy 'dynamic'; the node refuses to start if such a profile is configured but this flag is empty. - ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ===================================================================================================================================================================================================================================================================================================================================================== + ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== =============================================================================================================================================================================================================================================================================================================================================================================================================== + Key Default Description + ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== =============================================================================================================================================================================================================================================================================================================================================================================================================== + configfile ./config/nuts.yaml Nuts config file + cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. + datadir ./data Directory where the node stores its files. + didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used. + internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. + loggerformat text Log format (text, json) + strictmode true When set, insecure settings are forbidden. + url Public facing URL of the server (required). Must be HTTPS when strictmode is set. + verbosity info Log level (trace, debug, info, warn, error) + httpclient.timeout 30s Request time-out for HTTP clients, such as '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax. + **Auth** + auth.authorizationendpoint.enabled false enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature. + auth.experimental.jwtbearerclient false enables the experimental RFC 7523 jwt-bearer two-VP token request flow. While disabled (the default), requests carrying a service-provider subject identifier are rejected. Subject to change without notice. + **Crypto** + crypto.storage Storage to use, 'fs' for file system (for development purposes), 'vaultkv' for HashiCorp Vault KV store, 'azure-keyvault' for Azure Key Vault, 'external' for an external backend (deprecated). + crypto.azurekv.hsm false Whether to store the key in a hardware security module (HSM). If true, the Azure Key Vault must be configured for HSM usage. Default: false + crypto.azurekv.timeout 10s Timeout of client calls to Azure Key Vault, in Golang time.Duration string format (e.g. 10s). + crypto.azurekv.url The URL of the Azure Key Vault. + crypto.azurekv.auth.type default Credential type to use when authenticating to the Azure Key Vault. Options: default, managed_identity (see https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/azidentity/README.md for an explanation of the options). + crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. + crypto.vault.pathprefix kv The Vault path prefix. + crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). + crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. + **Discovery** + discovery.client.refreshinterval 10m0s Interval at which the client synchronizes with the Discovery Server; refreshing Verifiable Presentations of local DIDs and loading changes, updating the local copy. It only will actually refresh registrations of local DIDs that about to expire (less than 1/4th of their lifetime left). Specified as Golang duration (e.g. 1m, 1h30m). + discovery.definitions.directory ./config/discovery Directory to load Discovery Service Definitions from. If not set, the discovery service will be disabled. If the directory contains JSON files that can't be parsed as service definition, the node will fail to start. + discovery.server.ids [] IDs of the Discovery Service for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. + **HTTP** + http.clientipheader X-Forwarded-For Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs. + http.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). In strictmode, 'metadata-and-body' is not allowed and is changed to 'metadata' at startup. When debug vebosity is set the authorization headers are also logged when the request is fully logged. + http.cache.maxbytes 10485760 HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses. + http.client.allowedinternalcidrs [] 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 [] 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.internal.address 127.0.0.1:8081 Address and port the server will be listening to for internal-facing endpoints. + http.internal.auth.audience Expected audience for JWT tokens (default: hostname) + http.internal.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers + http.internal.auth.type Whether to enable authentication for /internal endpoints, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. + http.public.address \:8080 Address and port the server will be listening to for public-facing endpoints. + **JSONLD** + jsonld.contexts.localmapping [https://nuts.nl/credentials/2024=assets/contexts/nuts-2024.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://w3id.org/vc/status-list/2021/v1=assets/contexts/w3c-statuslist2021.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json,https://w3id.org/vc/status-list/2021/v1] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. + **PKI** + pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail + pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true + **Storage** + storage.debug false When true, enables extra logging of storage-layer problems (e.g. performance issues). + storage.session.memcached.address [] List of Memcached server addresses. These can be a simple 'host:port' or a Memcached connection URL with scheme, auth and other options. + storage.session.redis.address Redis session database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. If not set it, defaults to an in-memory database. + storage.session.redis.database Redis session database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. + storage.session.redis.password Redis session database password. If set, it overrides the username in the connection URL. + storage.session.redis.username Redis session database username. If set, it overrides the username in the connection URL. + storage.session.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. + storage.session.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. + storage.session.redis.sentinel.password Password for authenticating to Redis Sentinels. + storage.session.redis.sentinel.username Username for authenticating to Redis Sentinels. + storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). + storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL'). + storage.sql.rdsiam.dbuser Database username for IAM authentication. If not specified, the username from the connection string will be used. The database user must be created with IAM authentication enabled. + storage.sql.rdsiam.enabled false Enable AWS RDS IAM authentication for the SQL database connection. When enabled, the node will use temporary IAM tokens instead of passwords. Requires the connection string to be a PostgreSQL or MySQL RDS endpoint without a password. + storage.sql.rdsiam.region AWS region where the RDS instance is located (e.g., 'us-east-1). Required when RDS IAM authentication is enabled. + storage.sql.rdsiam.tokenrefreshinterval 14m0s Interval at which to refresh the IAM authentication token. RDS tokens are valid for 15 minutes, so set this to ensure tokens are refreshed before expiry. Specified as Golang duration (e.g. 10m, 1h). + **Tracing** + tracing.endpoint OTLP collector endpoint for OpenTelemetry tracing (e.g., 'localhost:4318'). When empty, tracing is disabled. + tracing.insecure false Disable TLS for the OTLP connection. + tracing.servicename Service name reported to the tracing backend. Defaults to 'nuts-node'. + **policy** + policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. + policy.authzen.endpoint Base URL of the AuthZen PDP endpoint. Required when any credential profile uses scope_policy 'dynamic'; the node refuses to start if such a profile is configured but this flag is empty. + ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== =============================================================================================================================================================================================================================================================================================================================================================================================================== diff --git a/http/cmd/cmd.go b/http/cmd/cmd.go index 3700e5d3b1..d2d0ce4154 100644 --- a/http/cmd/cmd.go +++ b/http/cmd/cmd.go @@ -34,7 +34,7 @@ func FlagSet() *pflag.FlagSet { flags.String("http.internal.auth.type", string(defs.Internal.Auth.Type), fmt.Sprintf("Whether to enable authentication for /internal endpoints, specify '%s' for bearer token mode or '%s' for legacy bearer token mode.", http.BearerTokenAuthV2, http.BearerTokenAuth)) flags.String("http.internal.auth.audience", defs.Internal.Auth.Audience, "Expected audience for JWT tokens (default: hostname)") flags.String("http.internal.auth.authorizedkeyspath", defs.Internal.Auth.AuthorizedKeysPath, "Path to an authorized_keys file for trusted JWT signers") - flags.String("http.log", string(defs.Log), fmt.Sprintf("What to log about HTTP requests. Options are '%s', '%s' (log request method, URI, IP and response code), and '%s' (log the request and response body, in addition to the metadata). When debug vebosity is set the authorization headers are also logged when the request is fully logged.", http.LogNothingLevel, http.LogMetadataLevel, http.LogMetadataAndBodyLevel)) + flags.String("http.log", string(defs.Log), fmt.Sprintf("What to log about HTTP requests. Options are '%s', '%s' (log request method, URI, IP and response code), and '%s' (log the request and response body, in addition to the metadata). In strictmode, '%s' is not allowed and is changed to '%s' at startup. When debug vebosity is set the authorization headers are also logged when the request is fully logged.", http.LogNothingLevel, http.LogMetadataLevel, http.LogMetadataAndBodyLevel, http.LogMetadataAndBodyLevel, http.LogMetadataLevel)) flags.String("http.clientipheader", defs.ClientIPHeaderName, "Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs.") flags.Int("http.cache.maxbytes", defs.ResponseCacheSize, "HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses.") flags.StringSlice("http.client.allowedinternalcidrs", defs.Client.AllowedInternalCIDRs, "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.") diff --git a/http/engine.go b/http/engine.go index 9ea89bb67a..d0527908f0 100644 --- a/http/engine.go +++ b/http/engine.go @@ -94,6 +94,13 @@ func (h *Engine) Configure(serverConfig core.ServerConfig) error { return err } + if serverConfig.Strictmode && h.config.Log == LogMetadataAndBodyLevel { + // Request/response bodies contain credentials: the OAuth token endpoint's client_assertion, + // VP tokens, authorization codes and issued access tokens. + log.Logger().Warn("Body logging (http.log=metadata-and-body) is not allowed in strictmode, falling back to metadata") + h.config.Log = LogMetadataLevel + } + h.applyTracingMiddleware(h.server) h.applyRateLimiterMiddleware(h.server, serverConfig) h.applyLoggerMiddleware(h.server, []string{MetricsPath, StatusPath, HealthPath}, h.config.Log) diff --git a/http/engine_test.go b/http/engine_test.go index b1e8101a75..7bb1536c9f 100644 --- a/http/engine_test.go +++ b/http/engine_test.go @@ -365,8 +365,10 @@ func TestEngine_LoggingMiddleware(t *testing.T) { engine := New(noop, nil) engine.config = createTestConfig() engine.config.Log = LogMetadataAndBodyLevel + serverConfig := core.NewServerConfig() + serverConfig.Strictmode = false - err := engine.Configure(*core.NewServerConfig()) + err := engine.Configure(*serverConfig) require.NoError(t, err) engine.Router().POST("/", func(c echo.Context) error { return c.JSON(200, "hello, world") @@ -385,6 +387,48 @@ func TestEngine_LoggingMiddleware(t *testing.T) { assert.Contains(t, output.String(), `HTTP response body: \"hello, world\"`) }) }) + t.Run("bodyLogger is disabled in strict mode", func(t *testing.T) { + // Configure sets the package-global client strict mode flag; restore it afterwards. + oldStrictMode := client.StrictMode + t.Cleanup(func() { client.StrictMode = oldStrictMode }) + warnOutput := new(bytes.Buffer) + logrus.StandardLogger().AddHook(&writer.Hook{ + Writer: warnOutput, + LogLevels: []logrus.Level{logrus.WarnLevel}, + }) + engine := New(noop, nil) + engine.config = createTestConfig() + engine.config.Log = LogMetadataAndBodyLevel + serverConfig := core.NewServerConfig() + serverConfig.Strictmode = true + + err := engine.Configure(*serverConfig) + require.NoError(t, err) + engine.Router().POST("/", func(c echo.Context) error { + return c.JSON(200, "very-secret-response") + }) + + err = engine.Start() + require.NoError(t, err) + defer engine.Shutdown() + + assertServerStarted(t, engine.config.Internal.Address) + + output.Reset() + response, err := http.Post("http://"+engine.config.Public.Address, "application/json", bytes.NewReader([]byte(`{"assertion": "very-secret-request"}`))) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + + logs := output.String() + // Metadata logging must still work: a request log entry with the response status. + assert.Contains(t, logs, `msg="HTTP request"`) + assert.Contains(t, logs, "status=200") + // Neither the request nor the response body content may reach the log. + assert.NotContains(t, logs, "very-secret-request") + assert.NotContains(t, logs, "very-secret-response") + assert.Equal(t, LogLevel(LogMetadataLevel), engine.config.Log) + assert.Contains(t, warnOutput.String(), "Body logging (http.log=metadata-and-body) is not allowed in strictmode, falling back to metadata") + }) } func assertServerStarted(t *testing.T, address string) { From 371f82c01bd8788bc7ba755eabdfce4e5a674790 Mon Sep 17 00:00:00 2001 From: Steven van der Vegt Date: Thu, 30 Jul 2026 16:47:33 +0200 Subject: [PATCH 2/2] docs: add release note for strictmode body logging fallback Assisted-by: AI --- docs/pages/release_notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index 563d39aa8c..90ea649a5e 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -14,6 +14,7 @@ Unreleased * #4233: ``request-credential`` API gains an optional ``credential_request_params`` JSON object overlaid on top of the OpenID4VCI Credential Request body sent to the issuer. Lets the wallet talk to issuers that accept additional fields, or to override the credential request entirely. ## Security +* #4440: In strictmode, ``http.log: metadata-and-body`` is no longer honored: the node resets it to ``metadata`` at startup and logs a warning. Full body logging wrote OAuth token endpoint request and response bodies (client assertions, VP tokens, authorization codes and issued access tokens) to the log at Info severity. Non-strictmode deployments are unaffected. By @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4440 * #4421: Stop reflecting fetched HTTP response bodies in API responses. The OAuth2 and OpenID4VCI callback handlers no longer place a remote endpoint's response body or error text into the returned ``error_description``, the did:web resolver no longer returns the fetched document body in its parse error, and the Discovery Service client no longer includes the remote server's error response in errors returned through the discovery APIs. Such content is now logged (truncated) for diagnostics instead. Static context such as the endpoint that failed is retained. By @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4421 * #4420: Harden the strict-mode HTTP client against SSRF. In strict mode the client now refuses at connect time to reach non-public addresses (loopback, private/RFC1918, unique local, link-local and unspecified), checked against the resolved IP so DNS-rebinding cannot bypass it, and refuses to follow a redirect that downgrades from HTTPS to HTTP. Cloud provider metadata endpoints are always blocked, following the OWASP SSRF prevention cheat sheet. Deployments that legitimately reach a private address for an internal flow (such as an internal credential offering or OAuth user flow) can permit specific ranges with ``http.client.allowedinternalcidrs``; publicly routable ranges that are internal-only can additionally be blocked with ``http.client.deniedcidrs``, which takes precedence. Reported by @raysabee, fixed by @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4420