feat: explicit proxy configuration for the CLI - #698
Draft
NickJosevski wants to merge 1 commit into
Draft
Conversation
Standard HTTP_PROXY/HTTPS_PROXY/NO_PROXY already worked for API traffic because the transport chain ends at http.DefaultTransport, but `octopus login --ignore-ssl-errors` built a bare http.Transport that dropped proxy support (and panicked when the client already had one). Adds an OCTOPUS_PROXY environment variable and matching ProxyUrl config key, which override HTTP_PROXY/HTTPS_PROXY for both schemes while still honouring NO_PROXY. Credentials may be embedded in the url or supplied via OCTOPUS_PROXY_USERNAME/OCTOPUS_PROXY_PASSWORD, which are read from the environment only so a password is never written to the config file, and are redacted in `config list`. socks5 comes free from net/http. Refs #49 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #49
Baseline: standard proxy env vars already work
Before adding anything, I checked whether the CLI loses Go's built-in proxy support. It does not, for normal API traffic.
pkg/apiclient/client_factory.go:129builds the http client withNewSpinnerRoundTripper(ask)pkg/apiclient/spinner_round_tripper.go:19setsNext: http.DefaultTransporthttp.DefaultTransport.Proxyishttp.ProxyFromEnvironmentSo
HTTP_PROXY,HTTPS_PROXYandNO_PROXYhave always been honoured for every Octopus API call. If that is all a customer needs, no CLI change was ever required. That reframes the issue: this is not "add proxy support", it is "add explicit configuration and close two gaps".The two real gaps
octopus login --ignore-ssl-errorslost the proxy.pkg/cmd/login/login.go:131built a bare&http.Transport{}, whoseProxyfield is nil — proxy support silently gone for exactly the command a new user runs first.httpClient.Transport.(*http.Transport)is an unchecked type assertion. When the CLI is already configured,f.GetHttpClient()returns the client whose transport is a*SpinnerRoundTripper, sooctopus login --ignore-ssl-errorscrashed withinterface conversion. Reproducible before this change; covered by a test now.What changed
New
pkg/apiclient/proxy.go:ProxySettings+ProxySettingsFromConfig()— reads the config/envProxyFunc()— resolution built ongolang.org/x/net/http/httpproxy(the same packagenet/httpuses), soNO_PROXYsemantics match the standard library exactlyNewHttpTransport(settings, insecureSkipVerify)— cloneshttp.DefaultTransportinstead of mutating it, keeping every standard default (including proxy) and no longer poisoning the process-wide transportRedactProxyUrl()— for displayWiring:
OCTOPUS_PROXYenv var +ProxyUrlconfig key (both were already stubbed out in commented-out code acrossconstants.go,config.go,config get,config set— this uncomments and completes them), plusconfig listsupport with redaction, and a fixedloginpath.Precedence
OCTOPUS_PROXYProxyUrlincli_config.jsonoctopus config set ProxyUrl ...HTTPS_PROXY/HTTP_PROXYAn explicit
OCTOPUS_PROXY/ProxyUrlapplies to both http and https requests (it replaces both env vars).NO_PROXYis honoured in every case, including over an explicit setting. Loopback targets are never proxied (standard Go behaviour, and what you want against a local Octopus).Credentials
user:pass@hostin the url works. Separately,OCTOPUS_PROXY_USERNAME/OCTOPUS_PROXY_PASSWORDapply to whichever proxy url was resolved — including one fromHTTPS_PROXY— and lose to credentials already in the url.Deliberately environment-only: they are read with
os.Getenv, not bound into viper, so they cannot be persisted tocli_config.jsonin plain text.config listredacts any password inProxyUrlviaurl.Redacted()(http://octo:xxxxx@proxy:3128), matching howApiKey/AccessTokenare already masked atpkg/cmd/config/list/list.go:38-44. The password is never logged or echoed.Out of scope, with reasons
http.Transportonly does Basic proxy auth. It would mean a third-party dependency (e.g.Azure/go-ntlmssp) doing a 3-leg handshake with connection affinity, plus SSPI for transparent single-sign-on on Windows. Real work, a supply-chain decision, and no test story without a Windows domain. Recommend a separate issue, driven by an actual customer request.net/http's transport dialssocks5://andsocks5h://proxy urls itself (socks_bundle.go,transport.go:1835). No extra dependency, no extra code.OCTOPUS_PROXY=socks5://host:1080works and is covered by a test.Test evidence
go build ./...clean.go test ./pkg/...all green (go vetreports 4 pre-existing "unreachable code" hits in unrelated files).pkg/apiclient/proxy_test.go— 14-case table over proxy resolution: no config,HTTP_PROXY/HTTPS_PROXYper scheme, explicit config overriding env, scheme-lesshost:port, socks5,NO_PROXYagainst both explicit and env proxies, loopback, and the three credential paths. Plus an invalid-url error case,ProxySettingsFromConfig, and aRedactProxyUrltable asserting the password never survives.TestNewHttpTransport_SendsRequestsThroughTheProxystands up anhttptestserver as the proxy and asserts the absolute-form request URI and theProxy-Authorization: Basicheader arrive at it.TestNewHttpTransport_LeavesTheDefaultTransportAloneguards the shared-transport mutation regression.pkg/cmd/login/login_test.go—TestConfigureHttpClientcovers all three branches, including the one that used to panic.pkg/config/config_test.go— provesOCTOPUS_PROXYis actually bound toProxyUrl.Every test is hermetic;
clearProxyEnvironmentstops the CI machine's own proxy settings leaking in.Open questions / options
1. Is an explicit setting wanted at all, or is env-only enough?
Since
HTTPS_PROXYalready worked,OCTOPUS_PROXYbuys one thing: pointing the CLI at a proxy without redirecting every other tool on the box. That is genuinely useful in CI, but it is new surface to document and support. Recommend keeping it — it is the thing the issue actually asks for, and it is cheap.2. No
--proxyflag, and there is a concrete reason.--proxyis already taken:pkg/machinescommon/proxy.go:15registers it ontarget ssh create,target listening-tentacle createand the worker equivalents, where it names an Octopus proxy resource. A root persistent--proxywould be shadowed by the local flag on exactly those commands — confusing for two different meanings of the word. Second obstacle: the client factory is built incmd/octopus/main.go:53, before cobra parses flags (the same ordering the spinner round-tripper comments call out), so a flag needs either lazy per-request resolution or a reordering. Options: (a) ship env/config only — my recommendation for this PR; (b) add--proxy-urlwith lazy resolution, ~10 lines on top of this; (c) reorder factory construction. Happy to do (b) if the team wants a flag.3. Credential env var names. The issue says
PROXY_USERNAME/PROXY_PASSWORD; I usedOCTOPUS_PROXY_USERNAME/OCTOPUS_PROXY_PASSWORDto match every otherOCTOPUS_*var. Unprefixed names risk colliding with other tooling. Easy to also accept the unprefixed names as a fallback if there is a compatibility reason.4. Should
ProxyUrlaccept credentials at all? It can today, andconfig listredacts it — but the password still sits incli_config.jsonin plain text, same asApiKeydoes. Alternative: reject a url containing a password onconfig setand force the env vars. Slightly more secure, slightly more annoying. Want that?5. Test matrix — what squid in Docker would add. The unit tests cover resolution and one real proxy hop, but not:
CONNECTtunnelling for https targets (thehttptestproxy sees an absolute URI, not aCONNECT), a 407 challenge/response round, proxies that mangle or buffer chunked responses, and TLS-terminating proxies with a corporate root CA. A squid container in CI would cover the first three; the fourth needs a generated CA and is where real customer pain usually lives. Suggest one squid-based integration test (anonymous + basic-auth) in the existing integration suite, kept out of the unit run. Worth noting the integration suite has its own CI problems today, so I did not add anything that depends on it.6. Unrelated but worth flagging:
pkg/apiclient/client_factory.go:124(before this change) setInsecureSkipVerify: trueunconditionally on the globalhttp.DefaultTransport— the CLI never verifies Octopus's TLS certificate, andlogin --ignore-ssl-errorsis effectively always on. I preserved the behaviour rather than change it in a proxy PR (it is now scoped to the CLI's own transport instead of the whole process), but it looks like a security bug and deserves its own issue.🤖 Generated with Claude Code