Skip to content

Commit d1b3b56

Browse files
TristanHoladaycmwylie19mjnagelrjferguson21
authored
fix: exemption race conditions (#407)
## Description Fixes race conditions with exemptions that result in overwrites of previous exemptions in the Pepr store or mutating then allowing pods that were meant to be exempted from mutation. ## Issue Fixes #409 Fixes #314 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Checklist before merging - [x] Test, docs, adr added or updated as needed - [ ] [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: Case Wylie <[email protected]> Co-authored-by: Micah Nagel <[email protected]> Co-authored-by: Rob Ferguson <[email protected]>
1 parent a62f9a0 commit d1b3b56

34 files changed

+773
-606
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "uds-core",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"description": "A collection of capabilities for UDS Core",
55
"keywords": [
66
"pepr",

pepr.ts

+32-12
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
1-
import { PeprModule } from "pepr";
1+
import { Log, PeprModule } from "pepr";
22

33
import cfg from "./package.json";
44

55
import { istio } from "./src/pepr/istio";
66
import { operator } from "./src/pepr/operator";
7-
import { policies } from "./src/pepr/policies";
7+
import { Policy } from "./src/pepr/operator/crd";
8+
import { registerCRDs } from "./src/pepr/operator/crd/register";
9+
import { policies, startExemptionWatch } from "./src/pepr/policies";
810
import { prometheus } from "./src/pepr/prometheus";
911

10-
new PeprModule(cfg, [
11-
// UDS Core Operator
12-
operator,
12+
(async () => {
13+
// Apply the CRDs to the cluster
14+
await registerCRDs();
15+
// KFC watch for exemptions and update in-memory map
16+
await startExemptionWatch();
17+
new PeprModule(cfg, [
18+
// UDS Core Operator
19+
operator,
1320

14-
// UDS Core Policies
15-
policies,
21+
// UDS Core Policies
22+
policies,
1623

17-
// Istio service mesh
18-
istio,
24+
// Istio service mesh
25+
istio,
1926

20-
// Prometheus monitoring stack
21-
prometheus,
22-
]);
27+
// Prometheus monitoring stack
28+
prometheus,
29+
]);
30+
// Remove legacy policy entries from the pepr store for the 0.5.0 upgrade
31+
if (process.env.PEPR_WATCH_MODE === "true" && cfg.version === "0.5.0") {
32+
Log.debug("Clearing legacy pepr store exemption entries...");
33+
policies.Store.onReady(() => {
34+
for (const p of Object.values(Policy)) {
35+
policies.Store.removeItem(p);
36+
}
37+
});
38+
}
39+
})().catch(err => {
40+
Log.error(err);
41+
process.exit(1);
42+
});

src/pepr/operator/common.ts

-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { Capability } from "pepr";
22

3-
// Register the CRD
4-
import "./crd/register";
5-
63
export const operator = new Capability({
74
name: "uds-core-operator",
85
description: "The UDS Operator is responsible for managing the lifecycle of UDS resources",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { beforeEach, describe, expect, it } from "@jest/globals";
2+
import { Matcher, MatcherKind, Policy } from "../../crd";
3+
import { ExemptionStore } from "./exemption-store";
4+
5+
const enforcerMatcher = {
6+
namespace: "neuvector",
7+
name: "^neuvector-enforcer-pod.*",
8+
kind: MatcherKind.Pod,
9+
};
10+
11+
const controllerMatcher = {
12+
namespace: "neuvector",
13+
name: "^neuvector-controller-pod.*",
14+
kind: MatcherKind.Pod,
15+
};
16+
17+
const getExemption = (uid: string, matcher: Matcher, policies: Policy[]) => {
18+
return {
19+
metadata: {
20+
uid,
21+
},
22+
spec: {
23+
exemptions: [
24+
{
25+
matcher,
26+
policies,
27+
},
28+
],
29+
},
30+
};
31+
};
32+
33+
describe("Exemption Store", () => {
34+
beforeEach(() => {
35+
ExemptionStore.init();
36+
});
37+
38+
it("Add exemption", async () => {
39+
const e = getExemption("uid", enforcerMatcher, [Policy.DisallowPrivileged]);
40+
ExemptionStore.add(e);
41+
const matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
42+
43+
expect(matchers).toHaveLength(1);
44+
});
45+
46+
it("Delete exemption", async () => {
47+
const e = getExemption("uid", enforcerMatcher, [Policy.DisallowPrivileged]);
48+
ExemptionStore.add(e);
49+
let matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
50+
expect(matchers).toHaveLength(1);
51+
ExemptionStore.remove(e);
52+
53+
matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
54+
expect(matchers).toHaveLength(0);
55+
});
56+
57+
it("Update exemption", async () => {
58+
const enforcerException = getExemption("uid", enforcerMatcher, [Policy.DisallowPrivileged]);
59+
ExemptionStore.add(enforcerException);
60+
61+
let matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
62+
expect(matchers).toHaveLength(1);
63+
64+
const controllerExemption = getExemption("uid", controllerMatcher, [Policy.RequireNonRootUser]);
65+
ExemptionStore.add(controllerExemption);
66+
67+
matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
68+
expect(matchers).toHaveLength(0);
69+
});
70+
71+
it("Add multiple policies", async () => {
72+
const enforcerException = getExemption("foo", enforcerMatcher, [Policy.DisallowPrivileged]);
73+
ExemptionStore.add(enforcerException);
74+
75+
let matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
76+
expect(matchers).toHaveLength(1);
77+
78+
const controllerExemption = getExemption("bar", controllerMatcher, [Policy.RequireNonRootUser]);
79+
ExemptionStore.add(controllerExemption);
80+
81+
matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
82+
expect(matchers).toHaveLength(1);
83+
84+
matchers = ExemptionStore.getByPolicy(Policy.RequireNonRootUser);
85+
expect(matchers).toHaveLength(1);
86+
});
87+
88+
it("Add duplicate exemptions owned by different owners", async () => {
89+
const enforcerException = getExemption("foo", enforcerMatcher, [Policy.DisallowPrivileged]);
90+
const otherEnforcerException = getExemption("bar", enforcerMatcher, [
91+
Policy.DisallowPrivileged,
92+
]);
93+
ExemptionStore.add(enforcerException);
94+
ExemptionStore.add(otherEnforcerException);
95+
96+
const matchers = ExemptionStore.getByPolicy(Policy.DisallowPrivileged);
97+
expect(matchers).toHaveLength(2);
98+
});
99+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Log } from "pepr";
2+
import { StoredMatcher } from "../../../policies";
3+
import { Matcher, Policy, UDSExemption } from "../../crd";
4+
5+
export type PolicyOwnerMap = Map<string, UDSExemption>;
6+
export type PolicyMap = Map<Policy, StoredMatcher[]>;
7+
let policyExemptionMap: PolicyMap;
8+
let policyOwnerMap: PolicyOwnerMap;
9+
10+
function init(): void {
11+
policyExemptionMap = new Map();
12+
policyOwnerMap = new Map();
13+
for (const p of Object.values(Policy)) {
14+
policyExemptionMap.set(p, []);
15+
}
16+
}
17+
18+
function getByPolicy(policy: Policy): StoredMatcher[] {
19+
return policyExemptionMap.get(policy) || [];
20+
}
21+
22+
function setByPolicy(policy: Policy, matchers: StoredMatcher[]): void {
23+
policyExemptionMap.set(policy, matchers);
24+
}
25+
26+
function addMatcher(matcher: Matcher, p: Policy, owner: string = ""): void {
27+
const storedMatcher = {
28+
...matcher,
29+
owner,
30+
};
31+
32+
const storedMatchers = getByPolicy(p);
33+
storedMatchers.push(storedMatcher);
34+
}
35+
36+
// Iterate through each exemption block of CR and add matchers to PolicyMap
37+
function add(exemption: UDSExemption, log: boolean = true) {
38+
// Remove any existing exemption for this owner, in case of WatchPhase.Modified
39+
remove(exemption);
40+
const owner = exemption.metadata?.uid || "";
41+
policyOwnerMap.set(owner, exemption);
42+
43+
for (const e of exemption.spec?.exemptions ?? []) {
44+
const policies = e.policies ?? [];
45+
for (const p of policies) {
46+
// Append the matcher to the list of stored matchers for this policy
47+
addMatcher(e.matcher, p, owner);
48+
if (log) {
49+
Log.debug(`Added exemption to ${p}: ${JSON.stringify(e.matcher)}`);
50+
}
51+
}
52+
}
53+
}
54+
55+
function remove(exemption: UDSExemption) {
56+
const owner = exemption.metadata?.uid || "";
57+
const prevExemption = policyOwnerMap.get(owner);
58+
59+
if (prevExemption) {
60+
for (const e of prevExemption.spec?.exemptions ?? []) {
61+
const policies = e.policies ?? [];
62+
for (const p of policies) {
63+
const existingMatchers = getByPolicy(p);
64+
const filteredList = existingMatchers.filter(m => {
65+
return m.owner !== owner;
66+
});
67+
setByPolicy(p, filteredList);
68+
}
69+
}
70+
policyOwnerMap.delete(owner);
71+
Log.debug(`Removed all policy exemptions for ${owner}`);
72+
} else {
73+
Log.debug(`No existing exemption for owner ${owner}`);
74+
}
75+
}
76+
77+
// export object with all included export as properties
78+
export const ExemptionStore = {
79+
init,
80+
add,
81+
remove,
82+
getByPolicy,
83+
};

0 commit comments

Comments
 (0)