Skip to content

Commit

Permalink
feat: add expose service entry for internal cluster traffic (#356)
Browse files Browse the repository at this point in the history
## Description

This adds a service entry to allow traffic to stay inside the cluster
and enable things like proper network policies when clients need to
access this endpoint.

## Related Issue

Fixes #N/A

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [X] Other (security config, docs update, etc)

## Checklist before merging

- [X] Test, docs, adr added or updated as needed
- [X] [Contributor Guide
Steps](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md)(https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md#submitting-a-pull-request)
followed

---------

Co-authored-by: Chance <139784371+UnicornChance@users.noreply.github.com>
Co-authored-by: Micah Nagel <micah.nagel@defenseunicorns.com>
  • Loading branch information
3 people authored May 14, 2024
1 parent e7cb33e commit 1bde4cc
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 126 deletions.
10 changes: 9 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@
"root": true,
"rules": {
"@typescript-eslint/no-floating-promises": ["error"]
}
},
"overrides": [
{
"files": [ "src/pepr/operator/crd/generated/**/*.ts", "src/pepr/operator/crd/generated/*.ts" ],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
]
}
2 changes: 1 addition & 1 deletion src/keycloak/chart/templates/secret-admin-password.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ apiVersion: v1
kind: Secret
metadata:
name: {{ $secretName }}
namespace: {{ .Release.Namespace }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "keycloak.labels" . | nindent 4 }}
type: Opaque
Expand Down
2 changes: 1 addition & 1 deletion src/keycloak/chart/templates/secret-postgresql.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: v1
kind: Secret
metadata:
name: {{ include "keycloak.fullname" . }}-postgresql
namespace: {{ .Release.Namespace }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "keycloak.labels" . | nindent 4 }}
type: Opaque
Expand Down
8 changes: 4 additions & 4 deletions src/pepr/operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The UDS Operator manages the lifecycle of UDS Package CRs and their correspondin
- establishing default-deny ingress/egress network policies
- creating a layered allow-list based approach on top of the default deny network policies including some basic defaults such as Istio requirements and DNS egress
- providing targeted remote endpoints network policies such as `KubeAPI` and `CloudMetadata` to make policies more DRY and provide dynamic bindings where a static definition is not possible
- creating Istio Virtual Services & related ingress gateway network policies
- creating Istio Virtual Services, Service Entries & related ingress gateway network policies

#### Exemption

Expand All @@ -25,7 +25,7 @@ metadata:
namespace: grafana
spec:
network:
# Expose rules generate Istio VirtualServices and related network policies
# Expose rules generate Istio VirtualServices, ServiceEntries and related network policies
expose:
- service: grafana
selector:
Expand Down Expand Up @@ -196,8 +196,8 @@ graph TD
G -->|Yes| H["Log: Skipping pkg"]
G -->|No| I["Update pkg status to Phase.Pending"]
I --> J{"Check if Istio is installed"}
J -->|Yes| K["Add injection label, process expose CRs for Virtual Services"]
J -->|No| L["Skip Virtual Service Creation"]
J -->|Yes| K["Add injection label, process expose CRs for Istio Resources"]
J -->|No| L["Skip Istio Resource Creation"]
K --> M["Create default network policies in namespace"]
L --> M
M --> N["Process allow CRs for network policies"]
Expand Down
92 changes: 92 additions & 0 deletions src/pepr/operator/controllers/istio/istio-resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { K8s, Log } from "pepr";

import { IstioVirtualService, IstioServiceEntry, UDSPackage } from "../../crd";
import { getOwnerRef } from "../utils";
import { generateVirtualService } from "./virtual-service";
import { generateServiceEntry } from "./service-entry";

/**
* Creates a VirtualService and ServiceEntry for each exposed service in the package
*
* @param pkg
* @param namespace
*/
export async function istioResources(pkg: UDSPackage, namespace: string) {
const pkgName = pkg.metadata!.name!;
const generation = (pkg.metadata?.generation ?? 0).toString();
const ownerRefs = getOwnerRef(pkg);

// Get the list of exposed services
const exposeList = pkg.spec?.network?.expose ?? [];

// Create a Set of processed hosts (to maintain uniqueness)
const hosts = new Set<string>();

// Track which ServiceEntries we've created
const serviceEntryNames: Map<string, boolean> = new Map();

// Iterate over each exposed service
for (const expose of exposeList) {
// Generate a VirtualService for this `expose` entry
const vsPayload = generateVirtualService(expose, namespace, pkgName, generation, ownerRefs);

Log.debug(vsPayload, `Applying VirtualService ${vsPayload.metadata?.name}`);

// Apply the VirtualService and force overwrite any existing policy
await K8s(IstioVirtualService).Apply(vsPayload, { force: true });

vsPayload.spec!.hosts!.forEach(h => hosts.add(h));

// Generate a ServiceEntry for this `expose` entry
const sePayload = generateServiceEntry(expose, namespace, pkgName, generation, ownerRefs);

// If we have already made a ServiceEntry with this name, skip (i.e. if advancedHTTP was used)
if (serviceEntryNames.get(sePayload.metadata!.name!)) {
continue;
}

Log.debug(sePayload, `Applying ServiceEntry ${sePayload.metadata?.name}`);

// Apply the ServiceEntry and force overwrite any existing policy
await K8s(IstioServiceEntry).Apply(sePayload, { force: true });

serviceEntryNames.set(sePayload.metadata!.name!, true);
}

// Get all related VirtualServices in the namespace
const virtualServices = await K8s(IstioVirtualService)
.InNamespace(namespace)
.WithLabel("uds/package", pkgName)
.Get();

// Find any orphaned VirtualServices (not matching the current generation)
const orphanedVS = virtualServices.items.filter(
vs => vs.metadata?.labels?.["uds/generation"] !== generation,
);

// Delete any orphaned VirtualServices
for (const vs of orphanedVS) {
Log.debug(vs, `Deleting orphaned VirtualService ${vs.metadata!.name}`);
await K8s(IstioVirtualService).Delete(vs);
}

// Get all related ServiceEntries in the namespace
const serviceEntries = await K8s(IstioServiceEntry)
.InNamespace(namespace)
.WithLabel("uds/package", pkgName)
.Get();

// Find any orphaned ServiceEntries (not matching the current generation)
const orphanedSE = serviceEntries.items.filter(
se => se.metadata?.labels?.["uds/generation"] !== generation,
);

// Delete any orphaned ServiceEntries
for (const se of orphanedSE) {
Log.debug(se, `Deleting orphaned ServiceEntry ${se.metadata!.name}`);
await K8s(IstioServiceEntry).Delete(se);
}

// Return the list of unique hostnames
return [...hosts];
}
53 changes: 53 additions & 0 deletions src/pepr/operator/controllers/istio/service-entry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from "@jest/globals";
import { UDSConfig } from "../../../config";
import { generateServiceEntry } from "./service-entry";
import { Expose, Gateway, IstioLocation, IstioResolution } from "../../crd";

describe("test generate service entry", () => {
const ownerRefs = [
{
apiVersion: "uds.dev/v1alpha1",
kind: "Package",
name: "test",
uid: "f50120aa-2713-4502-9496-566b102b1174",
},
];

const host = "test";
const port = 8080;
const service = "test-service";

const namespace = "test";
const pkgName = "test";
const generation = "1";

it("should create a simple ServiceEntry object", () => {
const expose: Expose = {
host,
port,
service,
};

const payload = generateServiceEntry(expose, namespace, pkgName, generation, ownerRefs);

expect(payload).toBeDefined();
expect(payload.metadata?.name).toEqual(`${pkgName}-${Gateway.Tenant}-${host}`);
expect(payload.metadata?.namespace).toEqual(namespace);

expect(payload.spec?.hosts).toBeDefined();
expect(payload.spec!.hosts![0]).toEqual(`${host}.${UDSConfig.domain}`);

expect(payload.spec!.location).toEqual(IstioLocation.MeshInternal);
expect(payload.spec!.resolution).toEqual(IstioResolution.DNS);

expect(payload.spec?.ports).toBeDefined();
expect(payload.spec!.ports![0].name).toEqual("https");
expect(payload.spec!.ports![0].number).toEqual(443);
expect(payload.spec!.ports![0].protocol).toEqual("HTTPS");

expect(payload.spec?.endpoints).toBeDefined();
expect(payload.spec!.endpoints![0].address).toEqual(
`${Gateway.Tenant}-ingressgateway.istio-${Gateway.Tenant}-gateway.svc.cluster.local`,
);
});
});
79 changes: 79 additions & 0 deletions src/pepr/operator/controllers/istio/service-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { UDSConfig } from "../../../config";
import { V1OwnerReference } from "@kubernetes/client-node";
import {
Expose,
Gateway,
IstioServiceEntry,
IstioLocation,
IstioResolution,
IstioPort,
IstioEndpoint,
} from "../../crd";
import { sanitizeResourceName } from "../utils";

/**
* Creates a ServiceEntry for each exposed service in the package
*
* @param pkg
* @param namespace
*/
export function generateServiceEntry(
expose: Expose,
namespace: string,
pkgName: string,
generation: string,
ownerRefs: V1OwnerReference[],
) {
const { gateway = Gateway.Tenant, host } = expose;

const name = generateSEName(pkgName, expose);

// For the admin gateway, we need to add the path prefix
const domain = (gateway === Gateway.Admin ? "admin." : "") + UDSConfig.domain;

// Append the domain to the host
const fqdn = `${host}.${domain}`;

const serviceEntryPort: IstioPort = {
name: "https",
number: 443,
protocol: "HTTPS",
};

const serviceEntryEndpoint: IstioEndpoint = {
// Map the gateway (admin, passthrough or tenant) to the ServiceEntry
address: `${gateway}-ingressgateway.istio-${gateway}-gateway.svc.cluster.local`,
};

const payload: IstioServiceEntry = {
metadata: {
name,
namespace,
labels: {
"uds/package": pkgName,
"uds/generation": generation,
},
// Use the CR as the owner ref for each ServiceEntry
ownerReferences: ownerRefs,
},
spec: {
// Append the UDS Domain to the host
hosts: [fqdn],
location: IstioLocation.MeshInternal,
resolution: IstioResolution.DNS,
ports: [serviceEntryPort],
endpoints: [serviceEntryEndpoint],
},
};

return payload;
}

export function generateSEName(pkgName: string, expose: Expose) {
const { gateway = Gateway.Tenant, host } = expose;

// Ensure the resource name is valid
const name = sanitizeResourceName(`${pkgName}-${gateway}-${host}`);

return name;
}
Loading

0 comments on commit 1bde4cc

Please sign in to comment.