-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(backend): Support running behind a corporate proxy [RHIDP-2217] (#…
…1225) * Add the undici and global-agent packages to the backend workspace Per [1], global-agent addresses node-fetch proxying, while undici afects the native fetch. [1] https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md * Add the type definitions package for global-agent Command used for reference: 'yarn add --dev @types/global-agent' Otherwise, we get the following errors when building the container images: //:tsc: cache miss, executing 7cdd96a5dd9f08b5 //:tsc: $ tsc //:tsc: packages/backend/src/index.ts:20:27 - error TS7016: Could not find a declaration file for module 'global-agent'. '/opt/app-root/src/node_modules/global-agent/dist/index.js' implicitly //:tsc: Try `npm i --save-dev @types/global-agent` if it exists or add a new declaration (.d.ts) file containing `declare module 'global-agent';` //:tsc: //:tsc: ~~~~~~~~~~~~~~ //:tsc: //:tsc: //:tsc: Found 1 error in packages/backend/src/index.ts:20 //:tsc: //:tsc: error Command failed with exit code 1. //:tsc: info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. //:tsc: ERROR: command finished with error: command (/opt/app-root/src/) /tmp/yarn--1714731744742-0.9188035523240228/yarn run tsc exited (1) //#tsc: command (/opt/app-root/src/) /tmp/yarn--1714731744742-0.9188035523240228/yarn run tsc exited (1) * Regen yarn.lock * Add proxy agent to allow backend requests to support corporate proxies [1] [1] https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md#installation * Add support for no_proxy excludes and explicitly bootstrap global-agent with no side-effect ref: https://gist.github.com/zicklag/1bb50db6c5138de347c224fda14286da * Document how to run the app behind a corporate proxy * Auto-set the GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE env var in the container image Setting it to an empty value allows global-agent to rely on the rather conventional HTTP(S)_PROXY and NO_PROXY env vars. [1] Otherwise, users would need to configure both GLOBAL_AGENT_HTTP(S)_PROXY and HTTP(S)_PROXY to make proxying work with both global-agent and other libs. Also, it might be a bit confusing for the user to have to set it explicitly to an empty value. We can set it the other way around, but users can still override it if needed. [1] https://github.com/gajus/global-agent#what-is-the-reason-global-agentbootstrap-does-not-use-http_proxy * Move configureCorporateProxyAgent to a separate file This would simplify the merging process in case of upstream changes Co-authored-by: Gennady Azarenkov <gazarenkov@redhat.com> * Document that proxy settings can also be specified in the operator default config Co-authored-by: Gennady Azarenkov <gazarenkov@redhat.com> * Add instructions for local development and testing in a separate 'docs/proxy.md' file Co-authored-by: Corey Daley <cdaley@redhat.com> * Document that plugin vendors have nothing to configure if they use the supported libraries for HTTP data fetching Co-authored-by: Kim Tsao <ktsao@redhat.com> * Apply suggestions from code review Co-authored-by: Corey Daley <cdaley@redhat.com> --------- Co-authored-by: Gennady Azarenkov <gazarenkov@redhat.com> Co-authored-by: Corey Daley <cdaley@redhat.com> Co-authored-by: Kim Tsao <ktsao@redhat.com>
- Loading branch information
1 parent
6a64308
commit 4cf349c
Showing
8 changed files
with
5,204 additions
and
2,877 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
# Local development behind a corporate proxy | ||
|
||
As mentioned in [Running the Showcase application behind a corporate proxy](../showcase-docs/corporate-proxy.md), the `HTTP(S)_PROXY` and `NO_PROXY` environment variables are supported. | ||
|
||
If you are behind a corporate proxy and are running the Showcase locally, as depicted in [Running locally with a basic configuration](../showcase-docs/getting-started.md#running-locally-with-a-basic-configuration) or [Running locally with the Optional Plugins](../showcase-docs/getting-started.md#running-locally-with-the-optional-plugins), you will need to additionally set the `GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE` to an empty value prior to running `yarn start`. | ||
|
||
Example: | ||
|
||
```shell | ||
$ GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE='' \ | ||
HTTP_PROXY=http://localhost:3128 \ | ||
HTTPS_PROXY=http://localhost:3128 \ | ||
NO_PROXY='localhost,example.org' \ | ||
yarn start | ||
``` | ||
|
||
You can use the command below to quickly start a local corporate proxy server (based on [Squid](https://www.squid-cache.org/)): | ||
|
||
```shell | ||
podman container run --rm --name squid-container \ | ||
-e TZ=UTC \ | ||
-p 3128:3128 \ | ||
-it docker.io/ubuntu/squid:latest | ||
``` | ||
|
||
# Plugin vendors | ||
|
||
The upstream Backstage project recommends the use of the `node-fetch` libraries in backend plugins for HTTP data fetching - see [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013/). | ||
|
||
We currently only support corporate proxy settings for Axios, `fetch` and `node-fetch` libraries. Backend plugins using any of these libraries have nothing special to do to support corporate proxies. | ||
|
||
# Testing | ||
|
||
The most challenging part of writing an end-to-end test from the context of a corporate proxy is to set up an environment where an application is forbidden access to the public Internet except through that proxy. | ||
|
||
One possible approach is to simulate such an environment in a Kubernetes namespace with the help of [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) to control ingress and egress traffic for pods within that namespace. | ||
|
||
To do so: | ||
|
||
1. Make sure the network plugin in your Kubernetes cluster supports network policies. [k3d](https://k3d.io) for example supports Network Policies out of the box. | ||
|
||
2. Create a separate proxy namespace, and deploy a [Squid](https://www.squid-cache.org/)-based proxy application there. The full URL to access the proxy server from within the cluster would be `http://squid-service.proxy.svc.cluster.local:3128`. | ||
|
||
```shell | ||
kubectl create namespace proxy | ||
|
||
cat <<EOF | kubectl -n proxy apply -f - | ||
apiVersion: v1 | ||
kind: Service | ||
metadata: | ||
name: squid-service | ||
labels: | ||
app: squid | ||
spec: | ||
ports: | ||
- port: 3128 | ||
selector: | ||
app: squid | ||
--- | ||
apiVersion: apps/v1 | ||
kind: Deployment | ||
metadata: | ||
name: squid-deployment | ||
spec: | ||
replicas: 1 | ||
selector: | ||
matchLabels: | ||
app: "squid" | ||
template: | ||
metadata: | ||
labels: | ||
app: squid | ||
spec: | ||
containers: | ||
- name: squid | ||
image: docker.io/ubuntu/squid:latest | ||
ports: | ||
- containerPort: 3128 | ||
name: squid | ||
protocol: TCP | ||
EOF | ||
``` | ||
|
||
3. Create the namespace where the Showcase application will be running, e.g.: | ||
|
||
```shell | ||
kubectl create namespace my-ns | ||
``` | ||
|
||
4. Add the network policies in the namespace above. The first one denies all egress traffic except to the DNS resolver and the Squid proxy. The second one allows ingress and egress traffic in the same namespace, because the Showcase app pod needs to contact the local Database pod. | ||
|
||
```shell | ||
cat <<EOF | kubectl -n my-ns apply -f - | ||
--- | ||
# Deny all egress traffic in this namespace => proxy settings can be used to overcome this. | ||
kind: NetworkPolicy | ||
apiVersion: networking.k8s.io/v1 | ||
metadata: | ||
name: default-deny-egress-with-exceptions | ||
spec: | ||
podSelector: {} | ||
policyTypes: | ||
- Egress | ||
egress: | ||
# allow DNS resolution (we need this allowed, otherwise we won't be able to resolve the DNS name of the Squid proxy service) | ||
- to: | ||
- namespaceSelector: | ||
matchLabels: | ||
kubernetes.io/metadata.name: kube-system | ||
podSelector: | ||
matchLabels: | ||
k8s-app: kube-dns | ||
ports: | ||
- port: 53 | ||
protocol: UDP | ||
- port: 53 | ||
protocol: TCP | ||
# allow traffic to Squid proxy | ||
- to: | ||
- namespaceSelector: | ||
matchLabels: | ||
kubernetes.io/metadata.name: proxy | ||
ports: | ||
- port: 3128 | ||
protocol: TCP | ||
--- | ||
kind: NetworkPolicy | ||
apiVersion: networking.k8s.io/v1 | ||
metadata: | ||
name: allow-same-namespace | ||
spec: | ||
podSelector: {} | ||
ingress: | ||
- from: | ||
- podSelector: {} | ||
egress: | ||
- to: | ||
- podSelector: {} | ||
EOF | ||
``` | ||
|
||
5. Follow the instructions to add the proxy environment variables for an [Operator-based](../showcase-docs/corporate-proxy.md#operator-deployment) or [Helm-based](../showcase-docs/corporate-proxy.md#helm-deployment) deployment. | ||
|
||
Example with a Custom Resource: | ||
|
||
```yaml | ||
apiVersion: rhdh.redhat.com/v1alpha1 | ||
kind: Backstage | ||
metadata: | ||
name: my-rhdh | ||
spec: | ||
application: | ||
# Support for Proxy settings added in PR 1225. Remove this once this PR is merged. | ||
# image: quay.io/janus-idp/backstage-showcase:pr-1225 | ||
appConfig: | ||
configMaps: | ||
- name: app-config-rhdh | ||
dynamicPluginsConfigMapName: dynamic-plugins-rhdh | ||
extraEnvs: | ||
envs: | ||
- name: HTTP_PROXY | ||
value: 'http://squid-service.proxy.svc.cluster.local:3128' | ||
- name: HTTPS_PROXY | ||
value: 'http://squid-service.proxy.svc.cluster.local:3128' | ||
- name: NO_PROXY | ||
value: 'localhost' | ||
- name: ROARR_LOG | ||
# Logs from global-agent (to inspect proxy settings) | ||
value: 'true' | ||
secrets: | ||
- name: secrets-rhdh | ||
# --- TRUNCATED --- | ||
``` |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// Copyright 2024 The Janus IDP Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
import { bootstrap } from 'global-agent'; | ||
import { Agent, ProxyAgent, Dispatcher, setGlobalDispatcher } from 'undici'; | ||
|
||
/** | ||
* Adds support for corporate proxy to both 'node-fetch' (using 'global-agent') and native 'fetch' (using 'undici') packages. | ||
* | ||
* Ref: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md | ||
* Ref: https://gist.github.com/zicklag/1bb50db6c5138de347c224fda14286da (to support 'no_proxy') | ||
*/ | ||
export function configureCorporateProxyAgent() { | ||
// Bootstrap global-agent, which addresses node-fetch proxy-ing. | ||
// global-agent purposely uses namespaced env vars to prevent conflicting behavior with other libraries, | ||
// but user can set GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value for global-agent to use | ||
// the conventional HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables. | ||
// More details in https://github.com/gajus/global-agent#what-is-the-reason-global-agentbootstrap-does-not-use-http_proxy | ||
bootstrap(); | ||
|
||
// Configure the undici package, which affects the native 'fetch'. It leverages the same env vars used by global-agent, | ||
// or the more conventional HTTP(S)_PROXY ones. | ||
const proxyEnv = | ||
process.env.GLOBAL_AGENT_HTTP_PROXY ?? | ||
process.env.GLOBAL_AGENT_HTTPS_PROXY ?? | ||
process.env.HTTP_PROXY ?? | ||
process.env.http_proxy ?? | ||
process.env.HTTPS_PROXY ?? | ||
process.env.https_proxy; | ||
|
||
if (proxyEnv) { | ||
const proxyUrl = new URL(proxyEnv); | ||
|
||
// Create an access token if the proxy requires authentication | ||
let token: string | undefined = undefined; | ||
if (proxyUrl.username && proxyUrl.password) { | ||
const b64 = Buffer.from( | ||
`${proxyUrl.username}:${proxyUrl.password}`, | ||
).toString('base64'); | ||
token = `Basic ${b64}`; | ||
} | ||
|
||
// Create a default agent that will be used for no_proxy origins | ||
const defaultAgent = new Agent(); | ||
|
||
// Create an interceptor that will use the appropriate agent based on the origin and the no_proxy | ||
// environment variable. | ||
// Collect the list of domains that we should not use a proxy for. | ||
// The only wildcard available is a single * character, which matches all hosts, and effectively disables the proxy. | ||
const noProxyEnv = | ||
process.env.GLOBAL_AGENT_NO_PROXY ?? | ||
process.env.NO_PROXY ?? | ||
process.env.no_proxy; | ||
const noProxyList = noProxyEnv?.split(',') || []; | ||
|
||
const isNoProxy = (origin?: string): boolean => { | ||
for (const exclusion of noProxyList) { | ||
if (exclusion === '*') { | ||
// Effectively disables proxying | ||
return true; | ||
} | ||
// Matched as either a domain which contains the hostname, or the hostname itself. | ||
if (origin === exclusion || origin?.endsWith(`.${exclusion}`)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
|
||
const noProxyInterceptor = ( | ||
dispatch: Dispatcher['dispatch'], | ||
): Dispatcher['dispatch'] => { | ||
return (opts, handler) => { | ||
return isNoProxy(opts.origin?.toString()) | ||
? defaultAgent.dispatch(opts, handler) | ||
: dispatch(opts, handler); | ||
}; | ||
}; | ||
|
||
// Create a proxy agent that will send all requests through the configured proxy, unless the | ||
// noProxyInterceptor bypasses it. | ||
const proxyAgent = new ProxyAgent({ | ||
uri: proxyUrl.protocol + proxyUrl.host, | ||
token, | ||
interceptors: { | ||
Client: [noProxyInterceptor], | ||
}, | ||
}); | ||
|
||
// Make sure our configured proxy agent is used for all `fetch()` requests globally. | ||
setGlobalDispatcher(proxyAgent); | ||
} | ||
} |
This file contains 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
Oops, something went wrong.