Skip to content
Open
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
14 changes: 7 additions & 7 deletions docs/reference/configuration/service-mesh/egress.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ UDS Core leverages Istio to route dedicated egress out of the service mesh. This

### Ambient

For workloads running in ambient mode, the dedicated egress gateway is automatically included in UDS Core. It comes pre-enabled and deploys waypoint workloads to the `istio-egress-waypoint` namespace.
For workloads running in ambient mode, the dedicated egress waypoint is automatically included in UDS Core. It comes pre-enabled and deploys waypoint workloads to the `istio-egress-ambient` namespace.

Additional configurations for the waypoint can be added in the form of helm overrides to the `uds-istio-egress-config` chart in the UDS Bundle, such as:

Expand Down Expand Up @@ -88,11 +88,11 @@ Wildcards in host names are NOT currently supported.
:::

:::caution
Adding any `remoteHost` egress creates/uses a shared L7 waypoint in ambient. This can change egress for other namespaces and break L4-only allowances (e.g., `remoteGenerated: Anywhere`), sometimes causing TLS failures. This behavior is by design in Istio ambient today.
Adding any `remoteHost` in Ambient creates/uses a shared L7 waypoint and a centralized, per‑host `ServiceEntry` + `AuthorizationPolicy` in `istio-egress-ambient`. Egress to that host is evaluated at L7. Namespaces without an AP for that host remain denied. This can change behavior for workloads that previously relied on L4‑only allowances (e.g., `remoteGenerated: Anywhere`) and may surface TLS/HTTP differences.

Recommendations:
- Prefer explicit `remoteHost` entries for required external hosts (even with broad L4 egress).
- Re‑verify critical egress after adding `remoteHost` in any namespace.
- Prefer explicit `remoteHost` entries and scope with `serviceAccount` (SA‑first).
- Re‑verify critical egress after adding or changing `remoteHost` in any namespace.
:::

### Ambient Mode
Expand Down Expand Up @@ -121,9 +121,9 @@ spec:
```

When a Package CR specifies the `network.allow` field with, at minimum, the `remoteHost` and `port` or `ports` parameters, the UDS Operator will create the necessary Istio resources to allow traffic to egress from the mesh. For ambient, the `serviceAccount` should be specified if your workload is not using the default service account. The resources that are created 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 AuthorizationPolicy, in the package namespace, which is used to enforce that only traffic from workloads using the selected service account can egress. If no `serviceAccount` is specified, the `default` service account is used.
* A shared Istio Waypoint, in the `istio-egress-ambient` namespace, which is used to route the egress traffic.
* A shared Istio ServiceEntry, in the `istio-egress-ambient` namespace, one per external host across all Ambient packages. This registers the external service (host and union of ports/protocols) and binds it to the egress waypoint.
* A centralized Istio AuthorizationPolicy, in the `istio-egress-ambient` namespace, that targets the per-host ServiceEntry (not the waypoint Gateway) and ALLOWs owners and Ambient "Anywhere" participants (ServiceAccount-first principal, else namespace). Rules use only `from:` sources; the destination host is implied by the ServiceEntry target.
* The shared Istio Waypoint (`Gateway`), in the `istio-egress-ambient` namespace, used to route ambient egress traffic.

#### Limitations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
reconcileService,
setupAmbientWaypoint,
} from "./ambient-waypoint";
import { ambientEgressNamespace, sharedEgressPkgId } from "./egress-ambient";
import { ambientEgressNamespace, sharedEgressPkgId } from "./istio-resources";

// Test helpers
const createMockPackage = (
Expand Down
8 changes: 6 additions & 2 deletions src/pepr/operator/controllers/istio/ambient-waypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { K8sGateway, K8sGatewayFromType, UDSPackage } from "../../crd";
import { Mode, Sso } from "../../crd/generated/package-v1alpha1";
import { PackageStore } from "../packages/package-store";
import { getAuthserviceClients, getOwnerRef } from "../utils";
import { ambientEgressNamespace, sharedEgressPkgId } from "./egress-ambient";
import { getSharedAnnotationKey, log } from "./istio-resources";
import {
ambientEgressNamespace,
getSharedAnnotationKey,
log,
sharedEgressPkgId,
} from "./istio-resources";
import { getWaypointName, matchesLabels, serviceMatchesSelector } from "./waypoint-utils";

export const egressWaypointName = "egress-waypoint";
Expand Down
190 changes: 73 additions & 117 deletions src/pepr/operator/controllers/istio/auth-policy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,130 +4,86 @@
*/

import { describe, expect, it } from "vitest";
import { RemoteProtocol } from "../../crd";
import {
generateAmbientEgressAuthorizationPolicy,
generateAmbientEgressAuthorizationPolicyName,
} from "./auth-policy";
import { generateLocalEgressSEName } from "./service-entry";
import { generateCentralAmbientEgressAuthorizationPolicy } from "./auth-policy";

describe("test generate authorization policy", () => {
it("should generate auth policy with service account", () => {
const pkgName = "test-pkg";
const host = "example.com";
const serviceEntryName = generateLocalEgressSEName(
pkgName,
[{ port: 443, protocol: RemoteProtocol.TLS }],
host,
);
const serviceAccount = "test-service-account";

const authPolicy = generateAmbientEgressAuthorizationPolicy(
host,
pkgName,
"test-ns",
"1",
[],
serviceEntryName,
serviceAccount,
);

expect(authPolicy.metadata?.name).toBe("example-com-test-service-account-egress");
expect(authPolicy.spec?.action).toBe("ALLOW");
expect(authPolicy.spec?.rules).toEqual(
expect.arrayContaining([
{
from: [
{
source: {
principals: [`cluster.local/ns/test-ns/sa/${serviceAccount}`],
},
},
],
},
]),
);
expect(authPolicy.spec?.targetRef).toEqual({
group: "networking.istio.io",
kind: "ServiceEntry",
name: serviceEntryName,
describe("test generate central ambient authorization policy", () => {
it("should generate central AP targeting ServiceEntry with from-only rules", () => {
const host = "example.com";
const generation = 1;
const saPrincipals = ["cluster.local/ns/ns1/sa/sa1", "cluster.local/ns/ns2/sa/sa2"];
const namespaces = ["ns3"]; // participant namespace

const ap = generateCentralAmbientEgressAuthorizationPolicy(host, generation, {
saPrincipals,
namespaces,
});

expect(ap.metadata?.namespace).toBe("istio-egress-ambient");
expect(ap.metadata?.name).toBe("ambient-ap-example-com");
expect(ap.spec?.action).toBe("ALLOW");
expect(ap.spec?.targetRef).toEqual({
group: "networking.istio.io",
kind: "ServiceEntry",
name: "ambient-se-example-com",
});

// from-only rules: contains principals and namespaces, no 'to'
const rules = ap.spec?.rules || [];
expect(rules.length).toBeGreaterThan(0);
for (const r of rules) {
expect(r).toHaveProperty("from");
expect(r).not.toHaveProperty("to");
}
// ensure sources captured
const flattenedSources = rules.flatMap(r => r.from || []);
const principalsEntry = flattenedSources.find(s => s.source?.principals);
const namespacesEntry = flattenedSources.find(s => s.source?.namespaces);
expect(principalsEntry?.source?.principals).toEqual(expect.arrayContaining(saPrincipals));
expect(namespacesEntry?.source?.namespaces).toEqual(expect.arrayContaining(namespaces));
});
});

it("should generate auth policy without service account", () => {
const pkgName = "test-pkg";
const host = "example.com";
const serviceEntryName = generateLocalEgressSEName(
pkgName,
[{ port: 443, protocol: RemoteProtocol.TLS }],
host,
);

const authPolicy = generateAmbientEgressAuthorizationPolicy(
host,
pkgName,
"test-ns",
"1",
[],
serviceEntryName,
undefined,
);
it("should generate per-port rules with to.operation.ports when identitiesByPort is provided", () => {
const host = "example.com";
const generation = 1;

expect(authPolicy.metadata?.name).toBe("example-com-egress");
expect(authPolicy.spec?.action).toBe("ALLOW");
expect(authPolicy.spec?.rules).toEqual(
expect.arrayContaining([
{
from: [
{
source: {
namespaces: ["test-ns"],
},
},
],
const identitiesByPort = {
"80": {
saPrincipals: ["cluster.local/ns/ns1/sa/http"],
namespaces: ["ns-http"],
},
"443": {
saPrincipals: ["cluster.local/ns/ns1/sa/https"],
namespaces: ["ns-https"],
},
]),
);
expect(authPolicy.spec?.targetRef).toEqual({
group: "networking.istio.io",
kind: "ServiceEntry",
name: serviceEntryName,
};

const ap = generateCentralAmbientEgressAuthorizationPolicy(
host,
generation,
{ saPrincipals: [], namespaces: [] },
undefined,
identitiesByPort,
);

const rules = ap.spec?.rules ?? [];
expect(rules).toHaveLength(2);

expect(rules[0].to?.[0]?.operation?.ports).toEqual(["80"]);
const r0Sources = (rules[0].from ?? []).map(f => f.source);
expect(r0Sources.some(s => s?.principals?.includes("cluster.local/ns/ns1/sa/http"))).toBe(
true,
);
expect(r0Sources.some(s => s?.namespaces?.includes("ns-http"))).toBe(true);

expect(rules[1].to?.[0]?.operation?.ports).toEqual(["443"]);
const r1Sources = (rules[1].from ?? []).map(f => f.source);
expect(r1Sources.some(s => s?.principals?.includes("cluster.local/ns/ns1/sa/https"))).toBe(
true,
);
expect(r1Sources.some(s => s?.namespaces?.includes("ns-https"))).toBe(true);
});
});
});

describe("test generate authorization policy name", () => {
it("should generate policy name with service account", () => {
const host = "example.com";
const serviceAccount = "test-service-account";

const name = generateAmbientEgressAuthorizationPolicyName(host, serviceAccount);

expect(name).toBe("example-com-test-service-account-egress");
});

it("should generate policy name without service account", () => {
const host = "example.com";

const name = generateAmbientEgressAuthorizationPolicyName(host, undefined);

expect(name).toBe("example-com-egress");
});

it("should sanitize host with special characters", () => {
const host = "api.example-service.com";
const serviceAccount = "my_service_account";

const name = generateAmbientEgressAuthorizationPolicyName(host, serviceAccount);

expect(name).toBe("api-example-service-com-my-service-account-egress");
});

it("should handle host with underscores and dots", () => {
const host = "db_host.internal.com";

const name = generateAmbientEgressAuthorizationPolicyName(host, undefined);

expect(name).toBe("db-host-internal-com-egress");
});
});
// Legacy per-namespace ambient authorization policy name tests removed.
Loading
Loading