Skip to content

feat: egress gw #1331

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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3f7ea9d
feat: egress gw
meganwolf0 Apr 24, 2025
5302b0d
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 Apr 24, 2025
54a9504
Merge branch 'main' into meganwolf0/feat-egress-gw
mjnagel Apr 29, 2025
cbd2d08
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 5, 2025
022eda7
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 7, 2025
9d077b5
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 8, 2025
9cd0a61
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 14, 2025
ffe4772
Update docs/reference/configuration/egress.md
meganwolf0 May 15, 2025
a662bd1
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 15, 2025
4795d88
fix: err handling
meganwolf0 May 15, 2025
fb06bc3
Update src/pepr/operator/controllers/istio/istio-resources.ts
meganwolf0 May 15, 2025
1dbebac
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 15, 2025
b115d3a
fix: formatting
meganwolf0 May 15, 2025
e58a3f2
fix: concurrency fixes
meganwolf0 May 19, 2025
58f5e0d
fix: fail on missing ports
meganwolf0 May 19, 2025
95ba83f
fix: tests
meganwolf0 May 23, 2025
7bd11d7
Merge remote-tracking branch 'origin/main' into meganwolf0/feat-egres…
meganwolf0 May 23, 2025
387bf99
fix: defaults
meganwolf0 May 23, 2025
3b65599
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 23, 2025
c8cc6a6
fix: logic
meganwolf0 May 23, 2025
d92b888
fix: dequeue fcn and test
meganwolf0 May 27, 2025
8b2841f
Merge branch 'main' into meganwolf0/feat-egress-gw
meganwolf0 May 27, 2025
0fb8cd9
fix: formatting
meganwolf0 May 27, 2025
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
1 change: 1 addition & 0 deletions bundles/k3d-standard/uds-bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ packages:
# x-release-please-end
optionalComponents:
- istio-passthrough-gateway
- istio-egress-gateway
- metrics-server
overrides:
pepr-uds-core:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ sidebar:
</tr>
</thead>
<tbody>
<tr><td style="white-space: nowrap;">description</td><td style="white-space: nowrap;">string</td><td>A description of the policy, this will become part of the policy name</td></tr><tr><td style="white-space: nowrap;">direction</td><td style="white-space: nowrap;">string (enum):<ul><li><code>Ingress</code></li><li><code>Egress</code></li></ul></td><td>The direction of the traffic</td></tr><tr><td style="white-space: nowrap;">labels</td><td style="white-space: nowrap;"></td><td>The labels to apply to the policy</td></tr><tr><td style="white-space: nowrap;">podLabels</td><td style="white-space: nowrap;"></td><td>Deprecated: use selector</td></tr><tr><td style="white-space: nowrap;">port</td><td style="white-space: nowrap;">number</td><td>The port to allow (protocol is always TCP)</td></tr><tr><td style="white-space: nowrap;">ports</td><td style="white-space: nowrap;">number[]</td><td>A list of ports to allow (protocol is always TCP)</td></tr><tr><td style="white-space: nowrap;">remoteCidr</td><td style="white-space: nowrap;">string</td><td>Custom generated policy CIDR</td></tr><tr><td style="white-space: nowrap;">remoteGenerated</td><td style="white-space: nowrap;">string (enum):<ul><li><code>KubeAPI</code></li><li><code>KubeNodes</code></li><li><code>IntraNamespace</code></li><li><code>CloudMetadata</code></li><li><code>Anywhere</code></li></ul></td><td>Custom generated remote selector for the policy</td></tr><tr><td style="white-space: nowrap;">remoteNamespace</td><td style="white-space: nowrap;">string</td><td>The remote namespace to allow traffic to/from. Use * or empty string to allow all namespaces</td></tr><tr><td style="white-space: nowrap;">remotePodLabels</td><td style="white-space: nowrap;"></td><td>Deprecated: use remoteSelector</td></tr><tr><td style="white-space: nowrap;">remoteSelector</td><td style="white-space: nowrap;"></td><td>The remote pod selector labels to allow traffic to/from</td></tr><tr><td style="white-space: nowrap;">remoteServiceAccount</td><td style="white-space: nowrap;">string</td><td>The remote service account to restrict incoming traffic from within the remote namespace. Only valid for Ingress rules.</td></tr><tr><td style="white-space: nowrap;">selector</td><td style="white-space: nowrap;"></td><td>Labels to match pods in the namespace to apply the policy to. Leave empty to apply to all pods in the namespace</td></tr>
<tr><td style="white-space: nowrap;">description</td><td style="white-space: nowrap;">string</td><td>A description of the policy, this will become part of the policy name</td></tr><tr><td style="white-space: nowrap;">direction</td><td style="white-space: nowrap;">string (enum):<ul><li><code>Ingress</code></li><li><code>Egress</code></li></ul></td><td>The direction of the traffic</td></tr><tr><td style="white-space: nowrap;">labels</td><td style="white-space: nowrap;"></td><td>The labels to apply to the policy</td></tr><tr><td style="white-space: nowrap;">podLabels</td><td style="white-space: nowrap;"></td><td>Deprecated: use selector</td></tr><tr><td style="white-space: nowrap;">port</td><td style="white-space: nowrap;">number</td><td>The port to allow (protocol is always TCP)</td></tr><tr><td style="white-space: nowrap;">ports</td><td style="white-space: nowrap;">number[]</td><td>A list of ports to allow (protocol is always TCP)</td></tr><tr><td style="white-space: nowrap;">remoteCidr</td><td style="white-space: nowrap;">string</td><td>Custom generated policy CIDR</td></tr><tr><td style="white-space: nowrap;">remoteGenerated</td><td style="white-space: nowrap;">string (enum):<ul><li><code>KubeAPI</code></li><li><code>KubeNodes</code></li><li><code>IntraNamespace</code></li><li><code>CloudMetadata</code></li><li><code>Anywhere</code></li></ul></td><td>Custom generated remote selector for the policy</td></tr><tr><td style="white-space: nowrap;">remoteHost</td><td style="white-space: nowrap;">string</td><td>Remote host to allow traffic out to</td></tr><tr><td style="white-space: nowrap;">remoteNamespace</td><td style="white-space: nowrap;">string</td><td>The remote namespace to allow traffic to/from. Use * or empty string to allow all namespaces</td></tr><tr><td style="white-space: nowrap;">remotePodLabels</td><td style="white-space: nowrap;"></td><td>Deprecated: use remoteSelector</td></tr><tr><td style="white-space: nowrap;">remoteProtocol</td><td style="white-space: nowrap;">string (enum):<ul><li><code>TLS</code></li><li><code>HTTP</code></li></ul></td><td>Protocol used for external connection</td></tr><tr><td style="white-space: nowrap;">remoteSelector</td><td style="white-space: nowrap;"></td><td>The remote pod selector labels to allow traffic to/from</td></tr><tr><td style="white-space: nowrap;">remoteServiceAccount</td><td style="white-space: nowrap;">string</td><td>The remote service account to restrict incoming traffic from within the remote namespace. Only valid for Ingress rules.</td></tr><tr><td style="white-space: nowrap;">selector</td><td style="white-space: nowrap;"></td><td>Labels to match pods in the namespace to apply the policy to. Leave empty to apply to all pods in the namespace</td></tr>
</tbody>
</table>
</div>
Expand Down
120 changes: 120 additions & 0 deletions docs/reference/configuration/egress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Istio Egress
---

UDS Core leverages Istio to route dedicated egress out of the service mesh. This document provides an overview and examples of the Istio resources that UDS Core deploys to handle egress.

:::note
This does not currently work with ambient mode enabled (`spec.network.serviceMesh.mode=ambient`) or with workloads that omit the sidecar proxy
:::

## Configuring the Egress Workload

The dedicated egress gateway is an *optional* component of UDS Core. To enable it in the UDS Bundle, add it to the `optionalComponents` as follows:

```yaml
kind: UDSBundle
metadata:
name: uds-core-bundle
description: My UDS Core Bundle
version: "0.1.0"

packages:
- name: uds-core
repository: oci://ghcr.io/defenseunicorns/packages/uds/core
version: "0.39.0"
optionalComponents:
- istio-egress-gateway
```

You will also need to configure any additional ports that you'd expect to egress to. 443 and 80 are default out of the box, but in the case of modifications you should use the `packages.overrides` as follows:

```yaml
overrides:
istio-egress-gateway:
gateway:
ports:
- name: status-port
port: 15021
protocol: TCP
targetPort: 15021
- name: http2
port: 80
protocol: TCP
targetPort: 80
- name: https
port: 443
protocol: TCP
targetPort: 443
- name: custom-port
port: 9200
protocol: TCP
targetPort: 9200
```

## Specifying Egress using the Package CR

The UDS Core Package Custom Resource (CR) is used to configure the egress workload. The egress routes are realized through the use of the `network.allow` - specifically the `remoteHost`, `remoteProtocol`, and `port` or `ports` parameters therein.

:::note
Currently, only HTTP and TLS protocols are supported. The configuration will default to TLS if not specified.
:::

:::note
Wildcards in host names are NOT currently supported.
:::

The following sample Package CR shows configuring egress to a specific host, "httpbin.org", on port 443.

```yaml
apiVersion: uds.dev/v1alpha1
kind: Package
metadata:
name: pkg-1
namespace: egress-gw-1
spec:
network:
allow:
- description: "Example Curl"
direction: Egress
port: 443
remoteHost: httpbin.org
remoteProtocol: TLS
selector:
app: curl
```

When a Package CR specifies the `network.allow` field with, at minimum, the `remoteHost` and `port` or `ports` parameters, the UDS Core operator will create the necessary Istio resources to allow traffic to egress from the mesh. These include the following:
* An Istio ServiceEntry, in the package namespace, which is used to define the external service that the workload can access.
* An Istio Sidecar, in the package namespace, which is used to enforce that only registered traffic can egress from the workload. This is only applied to the workload selected in the `network.allow`.
* A shared Istio VirtualService, in the istio egress gateway namespace, which is used to route the traffic to the egress gateway.
* A shared Istio Gateway, in the istio egress gateway namespace, which is used to expose the egress gateway to the outside world.
* A shared Istio Service Entry, in the istio egress gateway namespace, to register the hosts and the ports for the egress gateway.

## Limitations

The configuration in Package CRs in combination with the behavior of Istio should be understood when using egress. There are a few "gotchas" that might occur while using the egress configurations.

:::note
The following are not exhaustive and are subject to change as this implementation matures from sidecar to ambient.
:::

* Currently, egress will only work for workloads that are using the Istio sidecar proxy.

* Specifying a port in a Package that is not exposed via the workload: This will be allowed with a warning from the operator, but the traffic will not be able to egress. An `istioctl analyze` will show an error such as: `Referenced host:port not found: "egressgateway.istio-egress-gateway.svc.cluster.local:9200"`

* Specifying a remote host that is also used in other Gateways or VirtualServices: This will be allowed with a warning from the operator, but some unexpected behavior may occur. An `istioctl analyze` will show an error such as: `The VirtualServices ... define the same host ... which can lead to unexpected behavior` and `Conflict with gateways ...`

* For all egresses defined within a single Package CR, all workloads that also have egress will have shared access to any host defined (is that true with the VS?)

## Security Considerations

Additional security considerations to keep in mind when implementing egress:

* The TLS mode is PASSTHROUGH, this means that traffic will exit the mesh as-is. Without TLS origination, details like HTTP paths cannot be inspected, restricted or logged.

* Per Istio documentation: “The cluster administrator or the cloud provider must ensure that no traffic leaves the mesh bypassing the egress gateway. Mechanisms external to Istio must enforce this requirement” - Essentially, additional work may be needed to ensure traffic is actually egressing the cluster when and where it should be.

* Some potential vulnerabilities are introduced using TLS Passthrough - you’ll need to know what’s on the other side of that domain because of [domain fronting](https://en.wikipedia.org/wiki/Domain_fronting) - Essentially, this is only a safe feature for trusted hosts, or hosts you know are not vulnerable to domain fronting

* We are not blocking DNS exfiltration
17 changes: 10 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.5",
"husky": "9.1.7",
"jest": "29.7.0",
"lint-staged": "16.0.0",
Expand Down
5 changes: 5 additions & 0 deletions packages/base/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ components:
required: false
import:
path: ../../src/istio

- name: istio-egress-gateway
required: false
import:
path: ../../src/istio
5 changes: 5 additions & 0 deletions packages/standard/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ components:
import:
path: ../base

- name: istio-egress-gateway
required: false
import:
path: ../base

# Metrics Server
- name: metrics-server
required: false
Expand Down
17 changes: 17 additions & 0 deletions schemas/package-v1alpha1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@
"$ref": "#/definitions/RemoteGenerated",
"description": "Custom generated remote selector for the policy"
},
"remoteHost": {
"type": "string",
"description": "Remote host to allow traffic out to"
},
"remoteNamespace": {
"type": "string",
"description": "The remote namespace to allow traffic to/from. Use * or empty string to allow all\nnamespaces"
Expand All @@ -221,6 +225,10 @@
},
"description": "Labels to match pods in the namespace to apply the policy to. Leave empty to apply to all pods in the namespace\nThe labels to apply to the policy\nDeprecated: use selector\nDeprecated: use remoteSelector\nThe remote pod selector labels to allow traffic to/from\nSpecifies attributes for the client.\nLabels to match pods to automatically protect with authservice. Leave empty to disable authservice protection\nConfiguration options for the mapper.\nA template for the generated secret"
},
"remoteProtocol": {
"$ref": "#/definitions/RemoteProtocol",
"description": "Protocol used for external connection"
},
"remoteSelector": {
"type": "object",
"additionalProperties": {
Expand Down Expand Up @@ -1114,6 +1122,15 @@
"title": "RemoteGenerated",
"description": "Custom generated remote selector for the policy"
},
"RemoteProtocol": {
"type": "string",
"enum": [
"TLS",
"HTTP"
],
"title": "RemoteProtocol",
"description": "Protocol used for external connection"
},
"DerivePort": {
"type": "string",
"enum": [
Expand Down
5 changes: 5 additions & 0 deletions src/istio/values/base-egress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2024 Defense Unicorns
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial

service:
type: ClusterIP
2 changes: 2 additions & 0 deletions src/istio/values/base-istiod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ meshConfig:
envoyExtAuthzGrpc:
service: "authservice.authservice.svc.cluster.local"
port: "10003"
outboundTrafficPolicy:
mode: "ALLOW_ANY"

pilot:
env:
Expand Down
11 changes: 11 additions & 0 deletions src/istio/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,14 @@ components:
namespace: istio-passthrough-gateway
valuesFiles:
- "values/config-passthrough.yaml"

- name: istio-egress-gateway
required: false
charts:
- name: gateway
url: https://istio-release.storage.googleapis.com/charts
version: 1.24.3
releaseName: egressgateway
namespace: istio-egress-gateway
valuesFiles:
- "values/base-egress.yaml"
112 changes: 112 additions & 0 deletions src/pepr/operator/controllers/istio/defaultTestMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Copyright 2024 Defense Unicorns
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial
*/

import { jest } from "@jest/globals";
import { V1OwnerReference } from "@kubernetes/client-node";
import { K8s, kind } from "pepr";
import {
IstioGateway,
IstioServiceEntry,
IstioSidecar,
IstioVirtualService,
RemoteProtocol,
UDSPackage,
} from "../../crd";
import { PackageHostMap } from "./types";

export const pkgMock: UDSPackage = {
metadata: {
name: "test-package",
namespace: "test-namespace",
generation: 1,
},
spec: {
network: {
expose: [],
allow: [],
},
},
};

export const ownerRefsMock: V1OwnerReference[] = [
{
apiVersion: "uds.dev/v1alpha1",
kind: "Package",
name: "test-package",
uid: "f50120aa-2713-4502-9496-566b102b1174",
},
];

export const pkgHostMapMock: PackageHostMap = {
package1: {
"example.com": {
portProtocol: [{ port: 443, protocol: RemoteProtocol.TLS }],
},
},
};

export const defaultEgressMocks = {
applyGwMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
applyVsMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
applySeMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
applySidecarMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
getGwMock: jest.fn<() => Promise<{ items: IstioGateway[] }>>().mockResolvedValue({
items: [],
}),
getVsMock: jest.fn<() => Promise<{ items: IstioVirtualService[] }>>().mockResolvedValue({
items: [],
}),
getNsMock: jest.fn<() => Promise<kind.Namespace>>().mockResolvedValue({}),
deleteGwMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
deleteVsMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
deleteSeMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
deleteSidecarMock: jest.fn<() => Promise<void>>().mockResolvedValue(),
};

export function updateEgressMocks(egressMocks: Record<string, jest.Mock>) {
const baseImplementation = {
Apply: jest.fn<() => Promise<void>>().mockResolvedValue(),
InNamespace: jest.fn().mockReturnThis(),
Get: jest.fn(),
Logs: jest.fn(),
Delete: jest.fn(),
Watch: jest.fn(),
WithLabel: jest.fn(),
};

const mockK8s = jest.mocked(K8s);

// Define only the implementations for specific resources
const k8sImplementations = {
[IstioGateway.name]: {
...baseImplementation,
Get: egressMocks.getGwMock,
Apply: egressMocks.applyGwMock,
Delete: egressMocks.deleteGwMock,
},
[IstioVirtualService.name]: {
...baseImplementation,
Get: egressMocks.getVsMock,
Apply: egressMocks.applyVsMock,
Delete: egressMocks.deleteVsMock,
},
[IstioServiceEntry.name]: {
...baseImplementation,
Apply: egressMocks.applySeMock,
Delete: egressMocks.deleteSeMock,
},
[IstioSidecar.name]: {
...baseImplementation,
Apply: egressMocks.applySidecarMock,
Delete: egressMocks.deleteSidecarMock,
},
[kind.Namespace.name]: { ...baseImplementation, Get: egressMocks.getNsMock },
};

mockK8s.mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((model: any) => k8sImplementations[model.name] || baseImplementation) as any,
);
}
Loading