Add NO_PROXY support for automatic proxy bypass based on target host#114
Add NO_PROXY support for automatic proxy bypass based on target host#114ibozas wants to merge 1 commit intorundeck-plugins:masterfrom
Conversation
- Add 'No Proxy List' config parameter to all three providers (NodeExecutor, FileCopier, WinRMCheck) - Centralize proxy logic in configure_proxy() function in common.py - Delegate proxy routing to requests library via HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars - Add INFO-level log messages showing connection path (DIRECTLY vs via PROXY) - Maintain backward compatibility: proxy-only config preserves legacy explicit proxy behavior - Supports exact IPs, CIDR notation, domain suffixes, hostnames, and wildcard patterns
There was a problem hiding this comment.
Pull request overview
Adds configurable NO_PROXY behavior to the WinRM Python plugin so proxy routing can be automatically bypassed per target host/IP, with routing decisions delegated to requests via environment variables.
Changes:
- Added
winrmnoproxyconfig to NodeExecutor/FileCopier providers, and addedwinrmproxy+winrmnoproxyto the WinRMCheck provider. - Centralized proxy/no-proxy handling in
contents/common.pyvia a newconfigure_proxy()helper and integrated it into exec/filecopier/check flows. - Updated WinRMCheck to accept
--proxy/--noproxyargs and pass them through fromplugin.yaml.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
plugin.yaml |
Adds new proxy/no-proxy configuration properties and passes proxy args to winrm-check.py. |
contents/common.py |
Introduces configure_proxy() that sets proxy env vars, performs bypass check, and logs routing. |
contents/winrm-exec.py |
Reads RD_CONFIG_WINRMNOPROXY and uses common.configure_proxy() instead of explicit proxy=. |
contents/winrm-filecopier.py |
Reads RD_CONFIG_WINRMNOPROXY and uses common.configure_proxy() instead of explicit proxy=. |
contents/winrm-check.py |
Adds CLI args for proxy/no-proxy and calls common.configure_proxy(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy): | ||
| log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy)) | ||
| else: |
There was a problem hiding this comment.
The log message says "matched NO_PROXY" but prints the entire winrmnoproxy list, not the specific entry/pattern that matched. This is misleading (and differs from the PR description examples). Consider rewording to avoid implying a single match, or implement logic to identify which entry matched before logging it.
There was a problem hiding this comment.
I did this intentionally so the customer can now see whether the proxy connection is using DIRECT or PROXY. It's just an INFO log.
| - name: winrmnoproxy | ||
| title: No Proxy List | ||
| description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`" | ||
| type: String |
There was a problem hiding this comment.
Grammar/wording: "It can be overwriting at node level" should be "It can be overridden at node level" (and similarly for any other occurrences).
| - name: winrmnoproxy | ||
| title: No Proxy List | ||
| description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`" | ||
| type: String |
There was a problem hiding this comment.
Grammar/wording: "It can be overwriting at node level" should be "It can be overridden at node level".
| else: | ||
| if winrmnoproxy: | ||
| log.warning("noproxy is set but no proxy configured; noproxy ignored") | ||
| log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint) |
There was a problem hiding this comment.
This branch logs "DIRECTLY (no proxy configured)", but the request may still go through a proxy if the Rundeck process already has HTTP(S)_PROXY set in its environment (requests/pywinrm will honor env proxies by default). Consider either (a) adjusting the message to clarify it's only about plugin config (not actual routing), or (b) detecting existing env proxies and logging the effective routing more accurately.
| log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint) | |
| # No proxy configured via plugin; check for environment proxies that requests/pywinrm may still use | |
| http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy') | |
| https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') | |
| if http_proxy or https_proxy: | |
| log.info( | |
| "Connecting to %s via PROXY from environment (HTTP_PROXY=%s, HTTPS_PROXY=%s)" | |
| % (endpoint, http_proxy, https_proxy) | |
| ) | |
| else: | |
| log.info( | |
| "Connecting to %s DIRECTLY (no proxy configured in plugin or environment)" % endpoint | |
| ) |
| log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) | ||
| else: | ||
| # Legacy: explicit proxy for all connections | ||
| arguments["proxy"] = winrmproxy | ||
| log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) |
There was a problem hiding this comment.
The INFO log message includes the full proxy URL, which may contain embedded credentials (e.g., http://user:pass@host:port per the plugin config). This would leak secrets into Rundeck logs. Consider redacting userinfo before logging (and likewise for any debug logs).
| log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy)) | ||
|
|
||
| if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy): | ||
| log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy)) | ||
| else: | ||
| log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) | ||
| else: | ||
| # Legacy: explicit proxy for all connections | ||
| arguments["proxy"] = winrmproxy | ||
| log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) |
There was a problem hiding this comment.
log.debug("Proxy via env vars: HTTP(S)_PROXY=%s...") can also leak proxy credentials when the proxy URL contains user:pass. Even though it's debug-level, Rundeck debug logs are often collected centrally; please redact credentials (or avoid logging the full URL).
| log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy)) | |
| if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy): | |
| log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy)) | |
| else: | |
| log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) | |
| else: | |
| # Legacy: explicit proxy for all connections | |
| arguments["proxy"] = winrmproxy | |
| log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) | |
| log.debug("Proxy via env vars: HTTP(S)_PROXY set, NO_PROXY=%s" % winrmnoproxy) | |
| if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy): | |
| log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy)) | |
| else: | |
| log.info("Connecting to %s via PROXY" % endpoint) | |
| else: | |
| # Legacy: explicit proxy for all connections | |
| arguments["proxy"] = winrmproxy | |
| log.info("Connecting to %s via PROXY" % endpoint) |
VIDEO TESTING:
REC-20260304175703.mp4
Summary
configure_proxy()function incommon.pythat delegates proxy routing to Python'srequestslibrary via environment variables (HTTP_PROXY,HTTPS_PROXY,NO_PROXY), enabling automatic per-host proxy decisionsDIRECTLYvsvia PROXY), so operators can verify routing without enabling debug modeChanged files
contents/common.pyconfigure_proxy()function with env-var delegation,should_bypass_proxiescheck, and INFO-level loggingcontents/winrm-exec.pyRD_CONFIG_WINRMNOPROXYenv var, replaced explicit proxy assignment withconfigure_proxy()callcontents/winrm-filecopier.pycontents/winrm-check.py--proxyand--noproxyargparse arguments, addedimport common, integratedconfigure_proxy()callplugin.yamlwinrmnoproxyproperty to WinRMPython and WinRMcpPython providers; addedwinrmproxyandwinrmnoproxyto WinRMCheck provider; updated WinRMCheckscript-argsHow it works
When both Proxy and No Proxy List are configured:
HTTP_PROXY,HTTPS_PROXY, andNO_PROXYenvironment variables instead of passingproxy=directly towinrm.Session()requestslibrary (used internally by pywinrm) natively evaluates the target host againstNO_PROXYpatternsNO_PROXY, the connection goes direct; otherwise it routes through the proxyWhen only Proxy is configured (no No Proxy List):
winrm.Session()for all connectionsSupported NO_PROXY patterns
192.168.1.50192.168.1.0/24,10.0.0.0/8.internal.corpmyserver.domain.com*192.168.1.0/24,10.0.0.0/8,.internal.corpLog output examples
Target matches NO_PROXY (direct connection):
Target does NOT match NO_PROXY (proxy connection):
Testing