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: add policy exemptions #165

Merged
merged 69 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
48844ad
feat: add policy exemptions
TristanHoladay Feb 9, 2024
4f83cfc
wip: remove exp.yaml
TristanHoladay Feb 9, 2024
9356d2c
wip: update pepr to 0.25.0; use pepr .Reconcile() for UDSExemption; e…
TristanHoladay Feb 9, 2024
e47f5a0
wip: format and rename.
TristanHoladay Feb 9, 2024
46f4160
wip: refactored Exemption CRD to group multiple policies around singl…
TristanHoladay Feb 9, 2024
2bd2eb3
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 9, 2024
fa499e5
wip: refactor policies to getExemptionsFor(Policy) instead of hardcod…
TristanHoladay Feb 9, 2024
7bf6866
wip: fix policy key typo
TristanHoladay Feb 9, 2024
d870913
wip: create exemption CRs for neuvector, promtail, and prometheus.
TristanHoladay Feb 10, 2024
f472dc1
fix: exemption policy name typos
TristanHoladay Feb 10, 2024
26dbd51
wip: fix typo in promtail exemption.
TristanHoladay Feb 12, 2024
25785e8
fix: the CR class used by updateStatus depending on Kind; handle pote…
TristanHoladay Feb 12, 2024
e76173b
wip: wait for each write to pepr store; working on validator.
TristanHoladay Feb 12, 2024
4cd706d
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 12, 2024
6138215
wip: testing using a local map to update store instead of setItemAndW…
TristanHoladay Feb 12, 2024
867a927
wip: adding validation for Exemption.
TristanHoladay Feb 12, 2024
0360134
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 12, 2024
3287607
wip: remove exemptions.
TristanHoladay Feb 12, 2024
c1ff86d
wip: fix pepr format
TristanHoladay Feb 13, 2024
39a00af
wip: removed async await from getExemptionsFor()
TristanHoladay Feb 13, 2024
1483bb8
wip: remove old exemption files; cleanup.
TristanHoladay Feb 13, 2024
9ad5c6a
wip: update READMEs
TristanHoladay Feb 13, 2024
531e6da
wip: refactor registerExemptions into isExempt
TristanHoladay Feb 13, 2024
d92c380
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 13, 2024
2ac6229
wip: refactor exemption controller to handle updates to already deplo…
TristanHoladay Feb 13, 2024
1d2ce67
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 14, 2024
86f1c67
wip: cleaning up exemption controller; decoupling exemption controlle…
TristanHoladay Feb 14, 2024
b8b747d
wip: remove async await from processExemptions given current state; r…
TristanHoladay Feb 14, 2024
03f9622
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 15, 2024
4715314
wip: update exmpt to exempt; remove commented code.
TristanHoladay Feb 15, 2024
a070501
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 16, 2024
42d5ce6
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 19, 2024
c1c41b0
use UDSConfig to toggle allowing all namespaces for Exemptions in exe…
TristanHoladay Feb 20, 2024
55f4c4a
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 20, 2024
f1386be
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 21, 2024
9a8ff18
wip
TristanHoladay Feb 21, 2024
d00dfba
wip
TristanHoladay Feb 21, 2024
6816376
cleaning up yamllint errors
TristanHoladay Feb 21, 2024
7ab910a
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 21, 2024
35be349
wip: update delete exemptions to not use async; add case for update C…
TristanHoladay Feb 23, 2024
fbfa302
wip: fix lint errors
TristanHoladay Feb 24, 2024
bd4d507
pepr format
TristanHoladay Feb 24, 2024
f7aeffe
wip: handle case of multiple matcher removals from same policy.
TristanHoladay Feb 24, 2024
4918b62
wip: refactoring exemption controller and tests.
TristanHoladay Feb 26, 2024
c82601a
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 26, 2024
57fa970
wip: update exemption controller to allow duplicate entries from sepa…
TristanHoladay Feb 26, 2024
399941f
Merge branch 'main' into feat-exemptions
TristanHoladay Feb 26, 2024
592099a
wip: updating docs and error messaging.
TristanHoladay Feb 26, 2024
2e86b0c
pepr format
TristanHoladay Feb 27, 2024
8d33d64
adding mutate to all policies to annotate exempted resources.
TristanHoladay Feb 27, 2024
9eba26b
minor changes
TristanHoladay Feb 29, 2024
5ff921e
fix format
TristanHoladay Feb 29, 2024
22c84af
fixed isExempt tests; added markExemption().
TristanHoladay Feb 29, 2024
8e094b4
update markExemption to return mutate action
TristanHoladay Feb 29, 2024
34e333f
adding title and kind in exemption v1alpha1
TristanHoladay Feb 29, 2024
99077ca
fix lint
TristanHoladay Feb 29, 2024
c24026a
add validation for matcher kind; update pepr validate task pattern to…
TristanHoladay Feb 29, 2024
6ee92e1
Merge branch 'main' into feat-exemptions
TristanHoladay Mar 1, 2024
8cc4a58
organize imports on save
TristanHoladay Mar 4, 2024
5ff1d7f
merge main and resolve
TristanHoladay Mar 4, 2024
3060d6c
fix yamllint
TristanHoladay Mar 4, 2024
53f7a31
Merge branch 'main' into feat-exemptions
TristanHoladay Mar 4, 2024
4603517
Merge branch 'main' into feat-exemptions
TristanHoladay Mar 4, 2024
861ff30
Merge branch 'main' into feat-exemptions
TristanHoladay Mar 4, 2024
29ea2a3
merge main and resolve
TristanHoladay Mar 6, 2024
76670b1
minor refactors
TristanHoladay Mar 6, 2024
b5e91e1
using names for empty titles; moving files into common folders (valid…
TristanHoladay Mar 6, 2024
c5180e9
Merge branch 'main' into feat-exemptions
mjnagel Mar 6, 2024
1624994
file name changes; reverting yamllint config; formatting fix
TristanHoladay Mar 7, 2024
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
198 changes: 110 additions & 88 deletions package-lock.json

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions src/neuvector/chart/templates/uds-exemption.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
apiVersion: uds.dev/v1alpha1
kind: Exemption
metadata:
name: neuvector
namespace: uds-policy-exemptions
spec:
exemptions:
- policies:
- Disallow_Host_Namespaces
- Disallow_Privileged
- Require_Non_Root_User
- Drop_All_Capabilities
- Restrict_HostPath_Write
- Restrict_Volume_Types
matcher:
namespace: neuvector
name: "^neuvector-enforcer-pod.*"

- policies:
- Disallow_Privileged
- Require_Non_Root_User
- Drop_All_Capabilities
- Restrict_HostPath_Write
- Restrict_Volume_Types
matcher:
namespace: neuvector
name: "^neuvector-controller-pod.*"

- policies:
- Drop_All_Capabilities
matcher:
namespace: neuvector
name: "^neuvector-prometheus-exporter-pod.*"


# Neuvector mounts the following hostPaths as writeable:
# `/var/neuvector`: for Neuvector's buffering and persistent state


# Neuvector requires HostPath volume types
# Neuvector mounts the following hostPaths:
# `/var/neuvector`: (as writable) for Neuvector's buffering and persistent state
# `/var/run`: communication to docker daemon
# `/proc`: monitoring of processes for malicious activity
# `/sys/fs/cgroup`: important files the controller wants to monitor for malicious content
# https://github.com/neuvector/neuvector-helm/blob/master/charts/core/templates/enforcer-daemonset.yaml#L108

67 changes: 56 additions & 11 deletions src/pepr/operator/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
## UDS Operator

The UDS Operator manages the lifecycle of UDS Package CRs and their corresponding resources (e.g. NetworkPolicies, Istio VirtualServices, etc.). The operator uses [Pepr](https://pepr.dev) to bind the watch operations to the enqueue and reconciler. The operator is responsible for:
The UDS Operator manages the lifecycle of UDS Package CRs and their corresponding resources (e.g. NetworkPolicies, Istio VirtualServices, etc.) as well UDS Exemption CRs. The operator uses [Pepr](https://pepr.dev) to bind the watch operations to the enqueue and reconciler. The operator is responsible for:

#### Package
- enabling Istio sidecar injection in namespaces where the CR is deployed
- 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

#### Exemption
- updating the policies Pepr store with registered exemptions

### Example UDS Package CR

```yaml
Expand Down Expand Up @@ -41,21 +45,62 @@ spec:
description: "Tempo"
```

### Example UDS Exemption CR

```yaml
apiVersion: uds.dev/v1alpha1
kind: Exemption
metadata:
name: neuvector
namespace: uds-policy-exemptions
spec:
exemptions:
- policies:
- Disallow_Host_Namespaces
- Disallow_Privileged
- Require_Non_Root_User
- Drop_All_Capabilities
- Restrict_HostPath_Write
- Restrict_Volume_Types
matcher:
namespace: neuvector
name: "^neuvector-enforcer-pod.*"

- policies:
- Disallow_Privileged
- Require_Non_Root_User
- Drop_All_Capabilities
- Restrict_HostPath_Write
- Restrict_Volume_Types
matcher:
namespace: neuvector
name: "^neuvector-controller-pod.*"

- policies:
- Drop_All_Capabilities
matcher:
namespace: neuvector
name: "^neuvector-prometheus-exporter-pod.*"
```

### Key Files and Folders

```bash
.
├── controllers # Core business logic called by the reconciler
│   ├── istio # Manages Istio VirtualServices and sidecar injection for UDS Packages/Namespace
│   └── network # Manages default and generated NetworkPolicies for UDS Packages/Namespace
├── controllers # Core business logic called by the reconciler
│   ├── exemptions # Manages updating Pepr store with exemptions from UDS Exemption
│   ├── istio # Manages Istio VirtualServices and sidecar injection for UDS Packages/Namespace
│   └── network # Manages default and generated NetworkPolicies for UDS Packages/Namespace
├── crd
│   ├── generated # Type files generated by `uds run -f src/pepr/tasks.yaml gen-crds`
│   ├── sources # CRD source files
│   ├── register.ts # Registers the UDS Package CRD with the Kubernetes API
│   └── validator.ts # Validates UDS Package CRs with Pepr
├── enqueue.ts # Serializes UDS Package CRs for processing by the reconciler
├── index.ts # Entrypoint for the UDS Operator
└── reconciler.ts # Reconciles UDS Package CRs via the controllers
│   ├── generated # Type files generated by `uds run -f src/pepr/tasks.yaml gen-crds`
│   ├── sources # CRD source files
│   ├── exmpt-validator.ts # Validates UDS Exemption CRs with Pepr
│   ├── migrate.ts # Migrates older versions of UDS Package CRs to new version
│   ├── register.ts # Registers the UDS Package CRD with the Kubernetes API
│   └── validator.ts # Validates UDS Package CRs with Pepr
├── enqueue.ts # Serializes UDS Package CRs for processing by the reconciler
├── index.ts # Entrypoint for the UDS Operator
└── reconciler.ts # Reconciles UDS Package CRs via the controllers
```

### Flow
Expand Down
87 changes: 87 additions & 0 deletions src/pepr/operator/controllers/exemptions/exemptions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { afterAll, beforeAll, describe, expect, it, jest } from "@jest/globals";
import { Store } from "../../../policies/common";
import { processExemptions } from "./exemptions";
import { Policy } from "../../crd";
import { Exemption } from "../../crd/generated/exemption-v1alpha1";

const mockStore = new Map<string, string>();
const enforcerMatcher = { namespace: "neuvector", name: "^neuvector-enforcer-pod.*" };
const controllerMatcher = { namespace: "neuvector", name: "^neuvector-controller-pod.*" };
const prometheusMatcher = { namespace: "neuvector", name: "^neuvector-prometheus-exporter-pod.*" };
const mockExemption = {
spec: {
exemptions: [
{
matcher: enforcerMatcher,
policies: ["Disallow_Privileged", "Drop_All_Capabilities"],
},
{
matcher: controllerMatcher,
policies: ["Disallow_Privileged", "Drop_All_Capabilities"],
},
{
matcher: prometheusMatcher,
policies: ["Drop_All_Capabilities"],
},
],
},
};

describe("Test Exemptions Controller", () => {
beforeAll(() => {
jest.spyOn(Store, "getItem").mockImplementation((key: string) => {
return mockStore.get(key) || null;
});

jest.spyOn(Store, "setItem").mockImplementation((key: string, val: string) => {
mockStore.set(key, val);
});
});

afterAll(() => {
mockStore.clear();
jest.restoreAllMocks();
});

it("Add exemptions for the first time", async () => {
await processExemptions(mockExemption as Exemption);
expect(Store.getItem(Policy.DisallowPrivileged)).toEqual(
`[${JSON.stringify(enforcerMatcher)},${JSON.stringify(controllerMatcher)}]`,
);
expect(Store.getItem(Policy.DropAllCapabilities)).toEqual(
`[${JSON.stringify(enforcerMatcher)},${JSON.stringify(controllerMatcher)},${JSON.stringify(
prometheusMatcher,
)}]`,
);
});

it("Tries to add same exemptions again and doesn't", async () => {
await processExemptions(mockExemption as Exemption);
expect(Store.getItem(Policy.DisallowPrivileged)).toEqual(
`[${JSON.stringify(enforcerMatcher)},${JSON.stringify(controllerMatcher)}]`,
);
expect(Store.getItem(Policy.DropAllCapabilities)).toEqual(
`[${JSON.stringify(enforcerMatcher)},${JSON.stringify(controllerMatcher)},${JSON.stringify(
prometheusMatcher,
)}]`,
);
});

it("Removes exemptions when CR updates", async () => {
const mockExemption2 = {
spec: {
exemptions: [
...mockExemption.spec.exemptions,
{ matcher: enforcerMatcher, policies: ["Disallow_Privileged"] },
],
},
};
await processExemptions(mockExemption2 as Exemption);
expect(Store.getItem(Policy.DisallowPrivileged)).toEqual(
`[${JSON.stringify(enforcerMatcher)},${JSON.stringify(controllerMatcher)}]`,
);
expect(Store.getItem(Policy.DropAllCapabilities)).toEqual(
`[${JSON.stringify(controllerMatcher)},${JSON.stringify(prometheusMatcher)}]`,
);
});
});
114 changes: 114 additions & 0 deletions src/pepr/operator/controllers/exemptions/exemptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Log } from "pepr";
import { policies } from "../../../policies/index";
import { Matcher, Policy, UDSExemption } from "../../crd";

// Remove leading and trailing '/' if added by user to matcher name
function removeRegexSlash(name: string) {
if (name[0] === "/" && name[name.length - 1] === "/") {
name = name.slice(1, name.length - 1);
}
return name;
}

// *** Using setItemAndWait() ***

// Add Exemptions to Pepr store as "policy": "[{matcher}]"
// export async function addExemptions(exmpt: UDSExemption) {
// const t0 = performance.now();
// const { Store } = policies;
// if (exmpt.spec && exmpt.spec.exemptions) {
// for (const e of exmpt.spec.exemptions) {
// const name = removeRegexSlash(e.matcher.name);
// for (const p of e.policies) {
// const exemptionList = JSON.parse(Store.getItem(p) || "[]");
// exemptionList.push({ namespace: e.matcher.namespace, name: name });
// await Store.setItemAndWait(p, JSON.stringify(exemptionList));
// }
// }
// }
// const t1 = performance.now();
// Log.debug(`Time to complete exemption write: ${t1 - t0}`);
// }

function isAlreadyAdded(matchers: Matcher[], name: string) {
for (const m of matchers) {
if (m.name === name) {
return true;
}
}
}

const policyList = Object.values(Policy);

// *** Use Local Map to then Update Store ***
// Add Exemptions to Pepr store as "policy": "[{matcher}]"
export async function processExemptions(exmpt: UDSExemption) {
const t0 = performance.now();
const { Store } = policies;

// Aggregate matchers for each policy into local Map
const exemptionMap = new Map<Policy, Matcher[]>();
const exemptions = exmpt.spec?.exemptions ?? [];

// Iterate through each policy
for (const p of policyList) {
exemptionMap.set(p, JSON.parse(Store.getItem(p) || "[]"));

// Iterate through all exemption blocks
for (const e of exemptions) {
const matchers = exemptionMap.get(p) ?? [];
const name = removeRegexSlash(e.matcher.name);

if (e.policies.includes(p)) {
// Do additional checks if policy already has matchers
if (matchers.length > 0) {
if (isAlreadyAdded(matchers, name)) {
continue;
} else {
matchers.push({ namespace: e.matcher.namespace, name: name });
exemptionMap.set(p, matchers);
}
} else {
// Else add to policy for the first time
exemptionMap.set(p, [{ namespace: e.matcher.namespace, name: name }]);
}
} else {
// check if matcher should be removed from this policy because no longer in CR
for (const m of matchers) {
if (m.name === name) {
Log.debug(`Removing ${name} from ${p}`);
exemptionMap.set(
p,
matchers.filter(m => m.name !== name),
);
}
}
}
}
}

// Iterate through local Map and update Store
for (const [k, v] of exemptionMap.entries()) {
Log.debug(`Adding to policy ${k}: ${v}`);
Store.setItem(k, JSON.stringify(v));
}

const t1 = performance.now();
Log.debug(`Time to complete exemption write: ${t1 - t0}`);
}

export async function removeExemptions(exmpt: UDSExemption) {
const { Store } = policies;

if (exmpt.spec && exmpt.spec.exemptions) {
for (const e of exmpt.spec.exemptions) {
const name = removeRegexSlash(e.matcher.name);
for (const p of e.policies) {
const exemptionList: Matcher[] = JSON.parse(Store.getItem(p) || "[]");
//filter matchers, returning those that do not match current exemption.matcher.name
const filteredList = exemptionList.filter(m => m.name !== name);
await Store.setItemAndWait(p, JSON.stringify(filteredList));
}
}
}
}
30 changes: 30 additions & 0 deletions src/pepr/operator/crd/exmpt-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PeprValidateRequest } from "pepr";

import { UDSExemption } from ".";

const validNS = "uds-policy-exemptions";

export async function exmptValidator(req: PeprValidateRequest<UDSExemption>) {
const exmpt = req.Raw;
const ns = exmpt.metadata?.namespace;

if (ns !== validNS) {
return req.Deny(`Invalid namespace ${ns}; must be ${validNS}`);
}

const exemptions = exmpt.spec?.exemptions ?? [];
if (exemptions.length === 0) {
return req.Deny("Invalid number of exemptions: must have at least 1");
}

// Check that each matcher name is valid regex
for (const e of exemptions) {
try {
new RegExp(e.matcher.name);
} catch (err) {
return req.Deny(`Invalid regular expression pattern for ${e.matcher.name}: ${err}`);
}
}

return req.Approve();
}
Loading
Loading