Skip to content

feat(httpclient): trust additional CA certs from a directory#4359

Open
reinkrul wants to merge 7 commits into
masterfrom
feature/4285-httpclient-trust-bundle
Open

feat(httpclient): trust additional CA certs from a directory#4359
reinkrul wants to merge 7 commits into
masterfrom
feature/4285-httpclient-trust-bundle

Conversation

@reinkrul

@reinkrul reinkrul commented Jun 18, 2026

Copy link
Copy Markdown
Member

Closes #4285

Summary

The outbound HTTP client trusted only the OS CA bundle, so trusting an extra CA (e.g. the AORTA-GtK issuer) required rebuilding the Docker image with the cert baked in. This adds a config option to load additional CA certificates from a directory at startup.

Changes

  • Config: new httpclient.tls.extracertsdir. When set, all *.pem and *.crt files in the directory are parsed and added to the HTTP client trust bundle, on top of the OS CA bundle. Each cert is logged with its subject, SHA-256 fingerprint and type (root CA / intermediate CA / certificate) for audit.
  • Wiring: loaded in http.Engine.configureClient, mutating the shared client.SafeHttpTransport root pool. Affects the general clients (client.New / NewWithCache).
  • Docker: image creates /etc/nuts/http-trust.d (owned by the nuts user) and sets NUTS_HTTPCLIENT_TLS_EXTRACERTSDIR by default → drop PEM/CRT files in via a volume mount, no rebuild.
  • Docs: new server_options.rst row, documented the trust dir / env var in the Docker deployment guide, and regenerated config docs (make cli-docs).
  • Tests: loader unit tests (valid .pem+.crt, non-existent dir, non-cert extension ignored, malformed cert), cert classification, flag parsing, and an end-to-end test that stands up an HTTPS server with a custom-CA cert and verifies the client only connects after the CA is loaded.

Behavior

  • Empty config → feature off.
  • Configured-but-missing directory, or a malformed cert file → hard error at startup (fail fast rather than silently dropping an intended trust anchor).
  • In Docker the dir always exists; an empty dir is a no-op.

Scope notes

  • Separate from the gRPC trust bundle (tls.truststorefile) — out of scope per the issue.
  • A TLS client has a single trust-anchor pool (RootCAs); intermediates presented by the server are used for chain building during the handshake. All loaded certs go into that one pool (same as the gRPC truststore); the root/intermediate split is informational in the logs.
  • The mTLS path (NewWithTLSConfig, OpenID4VCI) replaces the whole TLS config and is intentionally unaffected.
  • No hot reload (out of scope).

🤖 Assisted by AI

Add the httpclient.tls.extracertsdir config option. When set, all *.pem
and *.crt files in the directory are loaded and added to the HTTP client
trust bundle, on top of the OS CA bundle. Subject and SHA-256 fingerprint
of each certificate are logged.

A configured-but-missing directory or an invalid certificate file is a
hard error at startup. The Docker image creates /etc/nuts/http-trust.d and
sets NUTS_HTTPCLIENT_TLS_EXTRACERTSDIR by default, so CAs can be dropped in
via a volume mount without rebuilding the image.

Closes #4285

Assisted by AI
@qltysh

qltysh Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on master by 0.01%.

Modified Files with Diff Coverage (3)

RatingFile% DiffUncovered Line #s
Coverage rating: B Coverage rating: C
http/engine.go55.6%70-71, 107-108
Coverage rating: C Coverage rating: C
core/server_config.go100.0%
New Coverage rating: B
http/client/trustbundle.go85.7%45-48, 57, 68-69
Total81.4%
🤖 Increase coverage with AI coding...
In the `feature/4285-httpclient-trust-bundle` branch, add test coverage for this new code:

- `http/client/trustbundle.go` -- Lines 45-48, 57, and 68-69
- `http/engine.go` -- Lines 70-71 and 107-108

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

reinkrul added 4 commits June 18, 2026 08:48
Add a test that starts an HTTPS server with a certificate signed by a
custom CA, confirms the client rejects it, then trusts it after loading
the CA via ConfigureTrustBundle.

Assisted by AI
The background refresh loop started by setupModule's Start() calls
ListDIDs on the subject when its registration is due, racing with the
subtest's own GetServiceActivation call and exceeding the Times(1)
expectation. Disable the loop in this subtest, matching the sibling
'activated, with VP' subtest.

Assisted by AI
Note the default /etc/nuts/http-trust.d directory and the
NUTS_HTTPCLIENT_TLS_EXTRACERTSDIR env var in the Docker volume mounts
section.

Assisted by AI
Log whether each loaded certificate is a root CA, intermediate CA or a
non-CA certificate, mirroring core.BuildTrustStore's classification. They
all share the single client RootCAs pool (a TLS client has no separate
intermediates pool); the distinction is informational for audit.

Assisted by AI
@reinkrul reinkrul marked this pull request as ready for review June 18, 2026 07:12
@qltysh

qltysh Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

0 new issues

Tool Category Rule Count

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow extending HTTP client trust bundle with additional CA certs from a directory

1 participant