Skip to content

feat: add outbound proxy support for the OTel Collector sidecar - #476

Open
WentingWu666666 wants to merge 13 commits into
documentdb:mainfrom
WentingWu666666:developer/wenting-otel-proxy-support
Open

WentingWu666666 wants to merge 13 commits into
documentdb:mainfrom
WentingWu666666:developer/wenting-otel-proxy-support

Conversation

@WentingWu666666

@WentingWu666666 WentingWu666666 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What

Adds routing-based outbound proxy support for the OpenTelemetry Collector sidecar injected into each DocumentDB pod, so its OTLP exporter egress can traverse an outbound proxy — Azure Arc Gateway and standard forward proxies. Routing is done via env vars. No image changes are required.

The collector's Go runtime honors the de-facto HTTP_PROXY / HTTPS_PROXY / NO_PROXY convention natively (http.ProxyFromEnvironment), so enabling the proxy is purely a matter of plumbing those env vars to the collector container.

Why

Azure Arc Gateway is a CONNECT-tunneling forward proxy with end-to-end TLS to the target (no MITM), so extension telemetry egress only needs routing — not a proxy certificate. This lets DocumentDB clusters run in proxy-restricted / Arc-enrolled environments.

How (config flow)

values.yaml -> operator Secret + env -> CNPG plugin params -> OTel Collector sidecar env:

Layer File Change
Values values.yaml New proxy block (isProxyEnabled, httpProxy, httpsProxy, noProxy)
Secret templates/08_proxy_secret.yaml (new) Hydrates documentdb-proxy-config in the operator namespace when enabled; keys are DOCUMENTDB_OTEL_HTTP_PROXY / DOCUMENTDB_OTEL_HTTPS_PROXY / DOCUMENTDB_OTEL_NO_PROXY (non-standard on purpose, see below)
Helpers templates/_helpers.tpl documentdb.proxySecretName; documentdb.noProxy always appends an in-cluster bypass list (localhost, .svc, .svc.cluster.local, RFC1918) so kube-apiserver, localhost Postgres scrapes and the in-pod OTLP receiver stay direct
Operator templates/09_documentdb_operator.yaml envFrom the Secret (the operator only forwards the values to the collector; it is never itself proxied) + checksum/proxy-config annotation to roll the operator on change
Operator (Go) constants.go, util.go Proxy env/param name constants + HTTPProxy/HTTPSProxy/NoProxy/ProxyEnabled helpers
Params cnpg_cluster.go, cnpg_sync.go Forward proxy params with the OTel sidecar params; track them for change-detection/rolling restart
Sidecar-injector config.go, lifecycle.go Parse the params; inject proxy env (UPPER + lowercase) onto the collector only when configured

Scope / follow-up

  • Proxy URLs with embedded credentials are not supported (they would be visible in the Cluster CR and pod env).

Testing

  • go build + go vet for both the operator and sidecar-injector modules
  • Go unit tests: config round-trip, proxyEnvVars, ProxyEnabled (+ existing cnpg/utils/controller and injector suites)
  • helm unittest: 116/116 pass, including a new proxy-secret suite and operator envFrom/checksum assertions
  • Rendered the chart with the proxy enabled to confirm NO_PROXY merges user + in-cluster defaults and the operator envFrom/annotation wire up

Values key / Azure Arc compatibility

The documented OSS key is proxy. A documentdb.proxy helper also honors Azure.proxySettings as a fallback solely for Azure Arc extension compatibility (the Arc platform injects that key), so no downstream fork or overlay is needed. proxy wins when both are set; isProxyEnabled is true if either enables it.

Out of scope: TLS-intercepting (SSL-bump) proxies / custom proxy CA. The OTLP exporter is currently hardcoded to plaintext gRPC, so this should be revisited together with exporter TLS support.

Operator is not proxied: the Secret keys use DOCUMENTDB_OTEL_* names instead of the standard ones so the operator's own Kubernetes API client never routes through the proxy, regardless of the cluster's Service CIDR. The collector sidecar still receives the standard HTTP_PROXY / HTTPS_PROXY / NO_PROXY.

wentingwu000 and others added 5 commits October 15, 2026 11:38
Route the per-cluster OTel Collector sidecar's OTLP exporter egress through
an outbound proxy (Azure Arc Gateway and standard forward proxies) using the
de-facto HTTP_PROXY / HTTPS_PROXY / NO_PROXY convention, which the collector's
Go runtime honors natively. No image changes are required.

Plumbing (Helm values -> operator env -> plugin params -> sidecar):
- values.yaml: new Azure.proxySettings block (isProxyEnabled, httpProxy,
  httpsProxy, noProxy, proxyCert reserved for a future phase).
- New 08_proxy_secret.yaml hydrates a proxy Secret in the operator namespace
  when enabled; NO_PROXY always appends an in-cluster bypass list so
  kube-apiserver, localhost Postgres scrapes and the in-pod OTLP receiver stay
  direct. Helpers documentdb.proxySecretName / documentdb.noProxy added.
- 09_documentdb_operator.yaml wires the Secret via envFrom (single source of
  truth) and rolls the operator on proxy-config change via a checksum
  annotation.
- constants.go / util.go: proxy env names, plugin-param names, and
  HTTPProxy/HTTPSProxy/NoProxy/ProxyEnabled helpers.
- cnpg_cluster.go forwards the proxy params alongside the OTel sidecar params;
  cnpg_sync.go tracks them so changes trigger a rolling restart.
- sidecar-injector config.go parses the params; lifecycle.go injects the
  proxy env (UPPER and lowercase) onto the collector only when configured.

Tests: config round-trip, proxyEnvVars, util ProxyEnabled, and helm-unittest
for the Secret and operator envFrom/checksum.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Document `proxy` as the OSS values key. Azure.proxySettings is honored only
as a compatibility fallback (Azure Arc injects it) via a documentdb.proxy
helper; `proxy` wins when both are set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
… unused helper, document credentials limit

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
@WentingWu666666
WentingWu666666 marked this pull request as ready for review October 19, 2026 17:37
Copilot AI balanced review requested due to automatic review settings October 19, 2026 17:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Proxy routing can disrupt Kubernetes API access and leave HTTP-only collector configurations attempting direct egress.

3 open issues
What changed in this PR

Adds outbound proxy configuration for the OTel Collector sidecar through Helm, the operator, and CNPG plugin parameters.

Changes:

  • Adds proxy values, Secret wiring, bypass defaults, and operator restart checksums.
  • Propagates proxy settings to collector containers and tracks configuration changes.
  • Adds Helm and Go tests for proxy configuration.
File Description
operator/​src/​internal/​utils/​util.go Reads proxy environment variables.
operator/​src/​internal/​utils/​util_test.go Tests proxy readers.
operator/​src/​internal/​utils/​constants.go Defines proxy environment and parameter names.
operator/​src/​internal/​cnpg/​cnpg_sync.go Tracks proxy parameter changes.
operator/​src/​internal/​cnpg/​cnpg_cluster.go Forwards proxy configuration.
operator/​documentdb-helm-chart/​values.yaml Documents proxy settings.
operator/​documentdb-helm-chart/​tests/​09_operator_deployment_test.yaml Tests Secret wiring and checksum presence.
operator/​documentdb-helm-chart/​tests/​08_proxy_secret_test.yaml Tests proxy Secret rendering and compatibility settings.
operator/​documentdb-helm-chart/​templates/​09_documentdb_operator.yaml Injects proxy settings and restart checksum.
operator/​documentdb-helm-chart/​templates/​08_proxy_secret.yaml Creates the proxy Secret.
operator/​documentdb-helm-chart/​templates/​_helpers.tpl Resolves settings and composes bypass entries.
operator/​cnpg-plugins/​sidecar-injector/​internal/​lifecycle/​lifecycle.go Injects collector proxy variables.
operator/​cnpg-plugins/​sidecar-injector/​internal/​lifecycle/​lifecycle_test.go Tests proxy variable generation.
operator/​cnpg-plugins/​sidecar-injector/​internal/​config/​config.go Parses and serializes proxy parameters.
operator/​cnpg-plugins/​sidecar-injector/​internal/​config/​config_test.go Tests proxy parameter round-tripping.

Review effort: Balanced

Comment thread operator/cnpg-plugins/sidecar-injector/internal/lifecycle/lifecycle.go Outdated
Comment thread operator/documentdb-helm-chart/values.yaml Outdated
@WentingWu666666
WentingWu666666 marked this pull request as draft October 20, 2026 01:05
wentingwu000 and others added 3 commits October 19, 2026 21:06
Merge proxyCert with the operator's system CA roots into the per-cluster
OTel ConfigMap and point the collector's SSL_CERT_FILE at the bundle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
The OTLP exporter is hardcoded to plaintext gRPC, so no collector
component performs TLS through a proxy and the merged CA bundle has no
working use case yet. Revisit together with exporter TLS support.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
@WentingWu666666
WentingWu666666 marked this pull request as ready for review October 21, 2026 16:52

@xgerman xgerman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified three proxy correctness gaps that should be addressed before merge:

  1. Kubernetes API bypass: applying HTTPS_PROXY to the operator can route its in-cluster API connection through the external proxy when KUBERNETES_SERVICE_HOST is IPv6 or outside the fixed RFC1918 ranges. Add the actual Kubernetes Service host to NO_PROXY. See #476 (comment)

  2. HTTP-only proxy configuration: grpc-go always evaluates gRPC targets using the HTTPS proxy selector, even when the OTLP exporter has tls.insecure: true. With only proxy.httpProxy configured, OTLP export remains direct. Populate the collector HTTPS_PROXY from httpProxy when httpsProxy is empty and test this case. See #476 (comment)

  3. Short in-cluster service names: the documented endpoint form otel-collector.monitoring:4317 matches neither .svc nor the RFC1918 entries. grpc-go checks NO_PROXY against the unresolved hostname, so this traffic can be sent to the external proxy. Require/document an FQDN or explicit noProxy entry, and narrow the claim that all in-cluster traffic is automatically bypassed. See #476 (comment)

The focused Go suites for the changed operator and injector packages pass; these are runtime proxy-selection gaps not covered by the current tests.

@xgerman

xgerman commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Runtime validation in kind

Tested commit 00cf873 on an arm64 kind Kubernetes 1.33.1 cluster using the PR-built operator/injector, OTel Collector 0.149.0 (grpc-go 1.79.3), and an in-cluster Squid CONNECT proxy.

What works

  • Helm rendered the proxy Secret, envFrom, and checksum correctly.
  • The PR operator and injector started successfully.
  • CNPG plugin parameters reached the injector, and the injected otel-collector received both uppercase and lowercase HTTPS_PROXY / NO_PROXY.
  • With HTTPS_PROXY set, the collector connected to the Squid service (10.96.159.25:3128), and Squid recorded CONNECT example.com:443.

Confirmed failures

The cluster does not have the Kubernetes 1.33 ImageVolume feature gate enabled, so the DocumentDB admission webhook rejected a full DocumentDB CR. I validated the injector path with a synthetic CNPG Cluster instead. The original Helm revision was restored and all test resources/images were removed after testing.

@hossain-rayhan

Copy link
Copy Markdown
Collaborator

1. HTTP-only proxy configuration does not work for the OTLP gRPC exporter

Location: operator/cnpg-plugins/sidecar-injector/internal/lifecycle/lifecycle.go:402-404

The chart accepts proxy.httpProxy without proxy.httpsProxy, but the Collector's OTLP exporter uses gRPC. gRPC-Go selects its proxy using HTTPS_PROXY, even when the destination connection uses plaintext gRPC.

With only proxy.httpProxy configured, the Collector receives HTTP_PROXY but no HTTPS_PROXY, so it attempts a direct connection instead of using the proxy. This breaks telemetry export in environments where direct egress is blocked.

Could we either use httpProxy as the Collector's fallback when httpsProxy is unset, or explicitly reject this configuration with a clear validation message? Please also add a transport-level test confirming that exports actually pass through the proxy; checking the injected environment variables alone would not catch this issue.

2. Mandatory RFC1918 bypass prevents proxying to private destinations

Location: operator/documentdb-helm-chart/templates/_helpers.tpl:132-147

The helper unconditionally appends 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 to NO_PROXY. These ranges cover private networks generally—not just this Kubernetes cluster.

For example, an OTLP destination at 10.20.30.40:4317 might be reachable only through the configured proxy. These defaults force a direct connection, and users cannot remove them because the helper always appends them.

Could we make the broad private-network bypasses configurable rather than mandatory? We can retain narrow defaults for loopback and cluster-local destinations while allowing users to control whether other private destinations bypass the proxy.

wentingwu000 and others added 4 commits October 21, 2026 15:27
The proxy Secret used the standard HTTP_PROXY/HTTPS_PROXY/NO_PROXY names
and was injected into the operator via envFrom, so the operator's
in-cluster client (which dials the literal KUBERNETES_SERVICE_HOST) could
be routed through the proxy when the API server IP was outside the
default NO_PROXY ranges (e.g. IPv6 or non-RFC1918 Service CIDRs).

Use DOCUMENTDB_OTEL_* names for the Secret keys and operator env. The
operator still forwards them to the collector sidecar, which exposes the
standard names. The operator process itself is never proxied.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
…roxy is set

grpc-go looks up the proxy with an https-scheme request, so it reads only
HTTPS_PROXY even for plaintext connections. With only httpProxy set the
OTLP exporter bypassed the proxy. Fall back to the HTTP proxy for
HTTPS_PROXY in the collector env and cover it with tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Short Service names such as otel-collector.monitoring are not covered by
the default bypass list; tell users to add them to proxy.noProxy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
@WentingWu666666

Copy link
Copy Markdown
Collaborator Author

@xgerman thanks for the thorough review. All three points are addressed:

  1. Kubernetes API bypass (cb2defa): Rather than extending NO_PROXY, the operator is no longer proxied at all. The chart now passes the settings to the operator as DOCUMENTDB_OTEL_HTTP_PROXY, DOCUMENTDB_OTEL_HTTPS_PROXY and DOCUMENTDB_OTEL_NO_PROXY. They are forwarded only to the collector sidecar, which still gets the standard HTTP_PROXY, HTTPS_PROXY and NO_PROXY. This removes the KUBERNETES_SERVICE_HOST problem entirely, for IPv6 and non-RFC1918 Service IPs alike.
  2. HTTP-only proxy (b17f36a): The collector's HTTPS_PROXY now falls back to httpProxy when httpsProxy is empty. An explicit httpsProxy is never overridden. Unit tests cover the HTTP-only and explicit-HTTPS cases.
  3. Short Service names (651f4a8): The values.yaml docs no longer claim all in-cluster traffic is bypassed. They state that matching is by hostname and that short names and custom hostnames must be added to proxy.noProxy.

All 116 helm tests and the Go suites for the touched packages pass. Could you take another look and clear the change request if you're satisfied?

@WentingWu666666

Copy link
Copy Markdown
Collaborator Author

@hossain-rayhan thanks for the review.

1. HTTP-only proxy: Fixed in b17f36a. The collector's HTTPS_PROXY/https_proxy now fall back to httpProxy when httpsProxy is empty (an explicit httpsProxy is never overridden), with unit tests and a values.yaml note.

You're right that env-var assertions can't prove the export actually goes through the proxy. A transport-level test (an e2e that routes the collector's OTLP export through a Squid pod and asserts the CONNECT) is planned as a follow-up rather than in this PR. I'll reply separately on point 2.

Add proxy.bypassPrivateRanges (default true). Setting it to false lets
private-network destinations, such as an OTLP endpoint reachable only
through the proxy, use the proxy. Cluster-local entries always bypass.
Azure.proxySettings is unchanged and always keeps the ranges.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5439e1a-20af-4212-9864-0c0863ae0ab5
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
@WentingWu666666

Copy link
Copy Markdown
Collaborator Author

2. RFC1918 bypass: Fixed in c4611fd. Added proxy.bypassPrivateRanges (default true, so existing behavior is unchanged). Setting it to false drops 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 from NO_PROXY, so a private OTLP endpoint reachable only through the proxy goes through it. Loopback and cluster-local entries (localhost, .svc, .svc.cluster.local, kubernetes.default.svc) always bypass. The option applies to proxy only; Azure.proxySettings is unchanged and keeps the ranges. Helm tests cover both.

This branch has not been deployed

No deployments
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.

6 participants