Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): Support running behind a corporate proxy [RHIDP-2217] #1225

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .rhdh/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ ENV NPM_CONFIG_ignore-scripts='true'
ENV SEGMENT_WRITE_KEY=gGVM6sYRK0D0ndVX22BOtS7NRcxPej8t
ENV SEGMENT_TEST_MODE=false

# RHIDP-2217: corporate proxy support (configured using 'global-agent')
# This is to avoid having to define several environment variables for the same purpose,
# i.e, GLOBAL_AGENT_HTTP(S)_PROXY (for 'global-agent') and the conventional HTTP(S)_PROXY (honored by other libraries like Axios).
# By setting GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value,
# 'global-agent' will use the same HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables.
ENV GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE=''

ENTRYPOINT ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.example.yaml", "--config", "app-config.example.production.yaml"]

# append Brew metadata here
7 changes: 7 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ ENV NPM_CONFIG_ignore-scripts='true'
ENV SEGMENT_WRITE_KEY=gGVM6sYRK0D0ndVX22BOtS7NRcxPej8t
ENV SEGMENT_TEST_MODE=false

# RHIDP-2217: corporate proxy support (configured using 'global-agent')
# This is to avoid having to define several environment variables for the same purpose,
# i.e, GLOBAL_AGENT_HTTP(S)_PROXY (for 'global-agent') and the conventional HTTP(S)_PROXY (honored by other libraries like Axios).
# By setting GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value,
# 'global-agent' will use the same HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables.
ENV GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE=''

ENTRYPOINT ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.example.yaml", "--config", "app-config.example.production.yaml"]

# append Brew metadata here
175 changes: 175 additions & 0 deletions docs/proxy.md
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 ---
```
5 changes: 4 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@
"app": "*",
"express": "4.19.2",
"express-prom-bundle": "6.6.0",
"global-agent": "^3.0.0",
"prom-client": "15.1.0",
"undici": "^6.15.0",
"winston": "3.11.0"
},
"devDependencies": {
"@backstage/cli": "0.26.4",
"@types/express": "4.17.21"
"@types/express": "4.17.21",
"@types/global-agent": "^2.1.3"
},
"files": [
"dist"
Expand Down
103 changes: 103 additions & 0 deletions packages/backend/src/corporate-proxy.ts
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 =
kim-tsao marked this conversation as resolved.
Show resolved Hide resolved
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({
kim-tsao marked this conversation as resolved.
Show resolved Hide resolved
uri: proxyUrl.protocol + proxyUrl.host,
token,
interceptors: {
Client: [noProxyInterceptor],
},
});

// Make sure our configured proxy agent is used for all `fetch()` requests globally.
setGlobalDispatcher(proxyAgent);
}
}
4 changes: 4 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
pluginIDProviderService,
rbacDynamicPluginsProvider,
} from './modules/rbacDynamicPluginsModule';
import { configureCorporateProxyAgent } from './corporate-proxy';

// RHIDP-2217: adds support for corporate proxy
configureCorporateProxyAgent();

const backend = createBackend();

Expand Down
Loading
Loading