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

fix: kubeapi watch updates, allow configurable cidr #1075

Merged
merged 17 commits into from
Dec 5, 2024
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
23 changes: 23 additions & 0 deletions docs/reference/configuration/uds-networking-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@
title: Networking Configuration
---

## KubeAPI Egress

The UDS operator is responsible for dynamically updating network policies that use the `remoteGenerated: KubeAPI` custom selector, in response to changes in the Kubernetes API server’s IP address. This ensures that policies remain accurate as cluster configurations evolve. However, in environments where the API server IP(s) frequently change, this behavior can lead to unnecessary overhead or instability.

To address this, the UDS operator provides an option to configure a static CIDR range. This approach eliminates the need for continuous updates by using a predefined range of IP addresses for network policies. To configure a specific CIDR range, set an override to `operator.KUBEAPI_CIDR` in your bundle as a value or variable. For example:

```yaml
packages:
- name: uds-core
repository: ghcr.io/defenseunicorns/packages/uds/core
ref: x.x.x
overrides:
uds-operator-config:
uds-operator-config:
values:
- path: operator.KUBEAPI_CIDR
value: "172.0.0.0/24"
```

This configuration directs the operator to use the specified CIDR range (`172.0.0.0/24` in this case) for KubeAPI network policies instead of dynamically tracking the API server’s IP(s).

When configuring a static CIDR range, it is important to make the range as restrictive as possible to limit the potential for unexpected networking access. An overly broad range could inadvertently allow egress traffic to destinations beyond the intended scope. Additionally, careful alignment with the actual IP addresses used by the Kubernetes API server is essential. A mismatch between the specified CIDR range and the cluster's configuration can result in network policy enforcement issues or disrupted connectivity.

## Additional Network Allowances

Applications deployed in UDS Core utilize [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) with a "Deny by Default" configuration to ensure network traffic is restricted to only what is necessary. Some applications in UDS Core allow for overrides to accommodate environment-specific requirements.
Expand Down
3 changes: 3 additions & 0 deletions src/pepr/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const UDSConfig = {
// Redis URI for Authservice
authserviceRedisUri,

// Static CIDR range to use for KubeAPI instead of k8s watch
kubeApiCidr: process.env.KUBEAPI_CIDR,

// Track if UDS Core identity-authorization layer is deployed
isIdentityDeployed: false,
};
Expand Down
5 changes: 5 additions & 0 deletions src/pepr/operator/controllers/network/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ export function generate(namespace: string, policy: Allow): kind.NetworkPolicy {
};
}

// Add the generated policy label (used to track KubeAPI policies)
if (policy.remoteGenerated) {
generated.metadata!.labels!["uds/generated"] = policy.remoteGenerated;
}

// Create the network policy peers
const peers: V1NetworkPolicyPeer[] = getPeers(policy);

Expand Down
261 changes: 261 additions & 0 deletions src/pepr/operator/controllers/network/generators/kubeAPI.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/**
* Copyright 2024 Defense Unicorns
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial
*/

import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { K8s, kind } from "pepr";
import { updateAPIServerCIDR } from "./kubeAPI";

type KubernetesList<T> = {
items: T[];
};

jest.mock("pepr", () => {
const originalModule = jest.requireActual("pepr") as object;
return {
...originalModule,
K8s: jest.fn(),
};
});

describe("updateAPIServerCIDR", () => {
const mockApply = jest.fn();
const mockGet = jest.fn<() => Promise<KubernetesList<kind.NetworkPolicy>>>();

beforeEach(() => {
jest.clearAllMocks();
(K8s as jest.Mock).mockImplementation(() => ({
WithLabel: jest.fn(() => ({
Get: mockGet,
})),
Apply: mockApply,
}));
});

it("handles a static CIDR string", async () => {
const mockService = {
spec: {
clusterIP: "10.0.0.1",
},
} as kind.Service;

const staticCIDR = "192.168.1.0/24";

// Mock the return of `Get` method
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: "0.0.0.0/0" } }],
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateAPIServerCIDR(mockService, staticCIDR);

expect(mockGet).toHaveBeenCalledWith();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: staticCIDR } }, { ipBlock: { cidr: "10.0.0.1/32" } }],
},
],
},
}),
{ force: true }, // Include the second argument in the call
);
});

it("handles an EndpointSlice with multiple endpoints", async () => {
const mockService = {
spec: {
clusterIP: "10.0.0.1",
},
} as kind.Service;

const mockSlice = {
endpoints: [{ addresses: ["192.168.1.2"] }, { addresses: ["192.168.1.3"] }],
} as kind.EndpointSlice;

// Mock the return of `Get` method
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: "0.0.0.0/0" } }],
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateAPIServerCIDR(mockService, mockSlice);

expect(mockGet).toHaveBeenCalledWith();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [
{ ipBlock: { cidr: "192.168.1.2/32" } },
{ ipBlock: { cidr: "192.168.1.3/32" } },
{ ipBlock: { cidr: "10.0.0.1/32" } },
],
},
],
},
}),
{ force: true }, // Include the second argument in the call
);
});

it("handles an empty EndpointSlice", async () => {
const mockService = {
spec: {
clusterIP: "10.0.0.1",
},
} as kind.Service;

const mockSlice = {
endpoints: [{}],
} as kind.EndpointSlice;

// Mock the return of `Get` method
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: "0.0.0.0/0" } }],
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateAPIServerCIDR(mockService, mockSlice);

expect(mockGet).toHaveBeenCalledWith();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: "10.0.0.1/32" } }],
},
],
},
}),
{ force: true }, // Include the second argument in the call
);
});

it("handles a Service with missing clusterIP", async () => {
const mockService = {
spec: {},
} as kind.Service;

const mockSlice = {
endpoints: [{ addresses: ["192.168.1.2"] }],
} as kind.EndpointSlice;

// Mock the return of `Get` method
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: "0.0.0.0/0" } }],
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateAPIServerCIDR(mockService, mockSlice);

expect(mockGet).toHaveBeenCalledWith();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: [{ ipBlock: { cidr: "192.168.1.2/32" } }],
},
],
},
}),
{ force: true }, // Include the second argument in the call
);
});

it("handles no matching NetworkPolicies", async () => {
const mockService = {
spec: {
clusterIP: "10.0.0.1",
},
} as kind.Service;

const mockSlice = {
endpoints: [{ addresses: ["192.168.1.2"] }],
} as kind.EndpointSlice;

// Mock the return of `Get` method to return no items
mockGet.mockResolvedValue({
items: [],
} as KubernetesList<kind.NetworkPolicy>);

await updateAPIServerCIDR(mockService, mockSlice);

expect(mockGet).toHaveBeenCalledWith();
expect(mockApply).not.toHaveBeenCalled();
});
});
Loading
Loading