feat(backend): global TLS CA bundle for outbound components [INFP-586] - #10487
feat(backend): global TLS CA bundle for outbound components [INFP-586]#10487fatih-acar wants to merge 9 commits into
Conversation
…mponent (INFP-586) Customers running a private PKI had to rebuild the Docker image to add their root CA, because git trusted only the system store and the credential helper and S3 storage had no CA setting at all, while cache, broker, database, HTTP, trace, syslog and LDAP each had their own. - New `tls.ca_bundle` section (`INFRAHUB_TLS_CA_BUNDLE`): a PEM file path that fills every component CA setting left unset. Precedence is component setting, then the global bundle, then the system store; a component with `tls_insecure` is left alone and the trace exporter only inherits it when its connection is already encrypted, since a bundle would switch a plaintext gRPC exporter to TLS. - Git: `git.tls_ca_file` and `git.tls_insecure`, written to the global git config as `http.sslCAInfo` / `http.sslVerify` at task-worker startup and cleared when unset so a persisted gitconfig cannot keep a stale value. The git config helpers move to `infrahub.git.global_config`. - Credential helper: the SDK client to the Infrahub API now honours `http.tls_*`. - S3 storage: `storage.s3.tls_ca_file` (`INFRAHUB_STORAGE_TLS_CA_FILE`, alias `AWS_CA_BUNDLE`) is passed to boto3 as `verify`. - Compose env blocks, configuration reference, a "Trust a private CA" guide, the git connect-repository page (no more custom image), and a dev knowledge page on outbound TLS. - Tests cover the precedence rules, the git config writes, the helper and S3 wiring, and an end-to-end clone over HTTPS from a server signed by a throwaway private CA. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 20 files
Confidence score: 4/5
- In
backend/infrahub/storage.py, initialization permitsAWS_S3_USE_SSL=Falsealongside a configured CA bundle, causing the endpoint to use plaintext while silently ignoring the CA setting; reject this incompatible configuration during initialization.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/infrahub/storage.py">
<violation number="1" location="backend/infrahub/storage.py:46">
P2: When `AWS_S3_USE_SSL=False` and a component or global CA bundle is configured, this endpoint is plaintext and the CA setting is silently ignored. Reject this incompatible combination during initialization so operators do not believe S3 certificate verification is active.
(Based on your team's feedback about rejecting TLS settings that are ignored for plaintext endpoints.)</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
| # Mirrors fastapi_storages.S3Storage.__init__, which offers no hook to pass a CA bundle to boto3. | ||
| if self.AWS_S3_ENDPOINT_URL.startswith("http"): | ||
| raise ValueError("AWS_S3_ENDPOINT_URL should not contain the protocol") | ||
| self._http_scheme = "https" if self.AWS_S3_USE_SSL else "http" |
There was a problem hiding this comment.
P2: When AWS_S3_USE_SSL=False and a component or global CA bundle is configured, this endpoint is plaintext and the CA setting is silently ignored. Reject this incompatible combination during initialization so operators do not believe S3 certificate verification is active.
(Based on your team's feedback about rejecting TLS settings that are ignored for plaintext endpoints.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/infrahub/storage.py, line 46:
<comment>When `AWS_S3_USE_SSL=False` and a component or global CA bundle is configured, this endpoint is plaintext and the CA setting is silently ignored. Reject this incompatible combination during initialization so operators do not believe S3 certificate verification is active.
(Based on your team's feedback about rejecting TLS settings that are ignored for plaintext endpoints.) </comment>
<file context>
@@ -36,7 +40,20 @@ def __init__(self, **kwargs: Any) -> None:
+ # Mirrors fastapi_storages.S3Storage.__init__, which offers no hook to pass a CA bundle to boto3.
+ if self.AWS_S3_ENDPOINT_URL.startswith("http"):
+ raise ValueError("AWS_S3_ENDPOINT_URL should not contain the protocol")
+ self._http_scheme = "https" if self.AWS_S3_USE_SSL else "http"
+ self._url = f"{self._http_scheme}://{self.AWS_S3_ENDPOINT_URL}"
+ self._s3 = boto3.resource(
</file context>
| self._http_scheme = "https" if self.AWS_S3_USE_SSL else "http" | |
| if self.AWS_CA_BUNDLE and not self.AWS_S3_USE_SSL: | |
| raise ValueError("AWS_CA_BUNDLE cannot be combined with AWS_S3_USE_SSL=False") | |
| self._http_scheme = "https" if self.AWS_S3_USE_SSL else "http" |
There was a problem hiding this comment.
Valid point — with use_ssl=false a configured CA bundle was passed to boto3 and silently ignored. An explicit INFRAHUB_STORAGE_TLS_CA_FILE / AWS_CA_BUNDLE with use_ssl=false is now rejected at startup by S3StorageSettings, the same rule the trace exporter applies to a plaintext endpoint, and the global INFRAHUB_TLS_CA_BUNDLE no longer fills S3 when the endpoint is plaintext, in c01f64f.
git built against GnuTLS with curl 8.x, which is what the shipped image uses, reports an untrusted HTTPS certificate as "server verification failed: certificate signer not trusted". The TLS error enrichment only matched the OpenSSL and older GnuTLS wordings, so the repository fell through to the generic RepositoryError with raw stderr and its operational status became `error` instead of `error-connection` with the certificate hint. The three wordings now live in one module-level tuple, the enrichment test covers each of them, and the HTTPS clone test asserts with that same tuple so the test and the product cannot drift. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ndle The task worker selects /opt/infrahub/.gitconfig by exporting GIT_CONFIG_GLOBAL in its own process, so `docker compose exec task-worker git config --global --get http.sslCAInfo` reads $HOME/.gitconfig instead and prints nothing. The guide and the connect-repository page now read the file named by INFRAHUB_GIT_GLOBAL_CONFIG_FILE explicitly and explain why, and the knowledge page records this trap along with the three git TLS failure wordings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 7 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 5 unresolved issues from previous reviews.
Re-trigger cubic
When the persisted global gitconfig already holds more than one http.sslCAInfo or http.sslVerify entry, a plain `git config <key> <value>` exits 5 with "cannot overwrite multiple values with a single value" and leaves the stale CA bundle or verification setting active. Write both TLS keys with `--replace-all`, passed before the key name because `git config` stops parsing options at the first positional argument, so startup always converges on the configured value. Tests seed duplicate entries and assert a single value remains. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 4 unresolved issues from previous reviews.
Re-trigger cubic
With use_ssl=false boto3 talks http:// and never reads verify=, so a configured CA bundle was passed through and silently ignored while the operator believed certificate verification was active. S3StorageSettings now rejects an explicit tls_ca_file with use_ssl=false at startup, the same rule the trace exporter applies to a plaintext endpoint, and Settings.apply_global_tls_ca_bundle no longer fills storage.s3 when the endpoint is plaintext. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A CA bundle that exists but cannot be read passed the is_file() check and then raised a raw OSError out of ssl.create_default_context, escaping the validator instead of producing the configuration error the other CA settings report. The shared _validate_ca_bundle_file helper now catches OSError alongside ssl.SSLError, like TraceSettings.validate_tls_configuration already does, so tls.ca_bundle and git.tls_ca_file fail validation consistently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…itconfig ownership The hardened .env template set INFRAHUB_TLS_CA_BUNDLE unconditionally, and every process validates that path at startup, so copying the template verbatim failed startup for anyone without the mount. The line now ships commented out with a pointer to the private CA guide. The apply_git_tls_config docstring, the connect-repository page and the dev knowledge page state that Infrahub owns http.sslCAInfo and http.sslVerify in its gitconfig and rewrites them at every task-worker startup, so operators use git.tls_ca_file / git.tls_insecure rather than editing the file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…artup git stops parsing options at the first positional argument, so a trailing `--replace-all` is read as the value pattern: it matches nothing and the write appends another `safe.directory = *` entry on every start of a worker with a persisted gitconfig. The call now uses the helper's `replace_all` flag, which places the option before the key. Two tests pin the behaviour: a plain set leaves duplicate values untouched, `replace_all` collapses them to the new value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nfiguration reference The generator dropped any optional scalar field (`str | None`, rendered by pydantic as an `anyOf` with a null option) that lives inside a nested section, because the nested handler only kept `anyOf` entries pointing at a `$ref`. `INFRAHUB_STORAGE_TLS_CA_FILE` was therefore missing from the reference. The non-null option's type is now used for nested and top-level optional scalars alike, which also replaces the `None` shown in the type column of every optional setting with its actual type. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 8 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would require human review. This introduces a broad outbound TLS trust policy and git certificate-verification controls across services. Human review is needed for the security scope and unresolved SDK environment-variable naming tradeoff.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would require human review. Introduces a global outbound TLS CA bundle and git TLS settings across components; requires human sign-off on the security scope and the unresolved INFRAHUB_TLS_CA_BUNDLE vs SDK INFRAHUB_TLS_CA_FILE naming overlap.
Re-trigger cubic
Summary
Operators running Infrahub behind a private PKI had to rebuild the Docker image to add their root CA: git trusted only the system store, and the git credential helper and S3 storage had no CA setting at all, while every other component carried its own. This PR adds one setting,
INFRAHUB_TLS_CA_BUNDLE, that every outbound TLS connection honours, closes the three gaps, and documents how to mount a bundle into the stock image.Resolves INFP-586 (split from INFP-434).
Key Changes
INFRAHUB_TLS_CA_BUNDLEpath and git, the HTTP client (webhooks, SSO, telemetry, task manager), Neo4j, the broker, the cache, S3, the trace exporter, log forwarding and LDAP all trust it. Precedence: component setting, then the global bundle, then the system store. A component with*_TLS_INSECUREis left alone.INFRAHUB_GIT_TLS_CA_FILEandINFRAHUB_GIT_TLS_INSECURE, written to the global git config at task-worker startup and cleared when unset, so a persisted gitconfig cannot keep a stale value.INFRAHUB_GIT_TLS_INSECUREreplaces the custom-imagehttp.sslVerify=falserecipe the docs used to teach.INFRAHUB_STORAGE_TLS_CA_FILE(aliasAWS_CA_BUNDLE), passed to boto3.Notable implementation details
Settingsvalidator that fills every unset component CA field, so adapters keep reading their own section and the resolved values are what shows up when inspecting settings.safe.directorywrite appended a duplicate entry on every worker start because a trailing--replace-allis parsed by git as a value pattern; it now uses the helper'sreplace_allflag. The configuration reference generator dropped optional scalar settings nested in a sub-section (the newINFRAHUB_STORAGE_TLS_CA_FILEwas invisible) and showedNoneas the type of every optional setting; both are fixed and the reference regenerated.Open question for reviewers: naming overlap with the SDK's
INFRAHUB_TLS_CA_FILEThe Python SDK's
Configuses theINFRAHUB_env prefix, so every SDK client built from the environment already readsINFRAHUB_TLS_CA_FILEandINFRAHUB_TLS_INSECURE(documented in the SDK reference). Inside the task-worker container that covers theinfrahubCLI,infrahubctl, generators or transforms that instantiateConfig()themselves, and the askpass helper. This PR introduces a server-sideINFRAHUB_TLS_CA_BUNDLEwith a broader meaning (every outbound connection), so two similarly named variables now coexist in the same container:INFRAHUB_TLS_CA_FILE(SDK): CA trusted when an SDK client talks to the Infrahub API. Not read by the server.INFRAHUB_TLS_CA_BUNDLE(server, this PR): CA trusted by every outbound connection of the server and task worker. Not read by SDK clients built from the environment, so setting only this variable leaves those SDK-based tools on the system trust store.Options considered:
INFRAHUB_TLS_CA_FILEso one variable configures both. It is also the more accurate name: the global is path-only, and most components already use*_TLS_CA_FILEfor a path while*_TLS_CA_BUNDLEis used where PEM text is accepted too. The cost is a scope expansion for anyone who already setsINFRAHUB_TLS_CA_FILEin the server environment to make the credential and askpass helpers reach a private-CA-served internal API: after the rename it would also apply to git, the database, the broker, the cache and S3. Those are the private-PKI users this feature targets, but it must be called out in the changelog.INFRAHUB_CA_BUNDLE: avoids theTLS_collision but keeps two variables with the same blindness as option 2.The PR is opened with option 2 to keep the change reviewable; option 1 is the recommended follow-up. Related gap found during this review:
git_credential/askpass.pystill builds its SDK client from the environment without the HTTP TLS settings the credential helper now applies; it should reusebuild_client_config()in the same follow-up.Documentation Updates
deploy-manage/install-configure/production-deployment/private-ca(Compose, Docker and Helm mounts, precedence, verification), registered in the sidebar.git-integration/connect-repository: the two custom-image sections now use the settings.production-deployment/overview: the TLS block shows the global bundle.reference/configurationregenerated;dev/knowledge/backend/tls.mdadded for contributors.Test Plan
verifywiring.git.tls_ca_file, and clones withgit.tls_insecure.error-connectionwith the certificate hint instead of a generic error; all three wordings are covered by the enrichment test.PREFECT_HOME(stale~/.prefectdatabase on the host). knip and the docs build could not run locally (Node 18 host, repo needs 24); sidebar id and links verified by script.INFRAHUB_TLS_CA_BUNDLE, thendocker compose exec task-worker git config --file /opt/infrahub/.gitconfig --get http.sslCAInfoprints the path (the worker selects that file throughGIT_CONFIG_GLOBALin its own process, so--globalfrom an exec shell reads a different file) and a repository on an internal Git server reachesActive.