Skip to content

Commit f3531a0

Browse files
author
Super User
committed
SPIRE-525: Add e2e tests for SCC hardening (PR #105) and enhance test generation rules
- Add Security hardening Context to e2e_test.go with two tests: - SCC field validation (AllowHostNetwork, AllowPrivilegeEscalation, etc.) - DaemonSet and pod security context verification (Privileged, ReadOnlyRootFilesystem, Capabilities.Drop) - Register securityv1 scheme in e2e_suite_test.go for SCC object access - Add SpireAgentSCCName constant to constants.go - Enhance e2e-rules.md Step 6 and Step 7 with structured test case IDs, priority levels, coverage gap fields, and per-step expected results - Add test plan document in output/pr-105/test-cases.md Made-with: Cursor
1 parent 1ed6eb9 commit f3531a0

5 files changed

Lines changed: 276 additions & 30 deletions

File tree

.cursor/rules/e2e-rules.md

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -147,37 +147,62 @@ For each scenario, decide:
147147
| Related file exists but different scenario | `new-in-file` -- add new `Context`/`It` in the existing file |
148148
| No match at all | `new-file` -- create new `*_test.go` (only if truly distinct) |
149149

150-
### Step 6: Prioritize (Top 10 most impactful)
150+
### Step 6: Generate Top 10 Most Impactful Missing Tests
151151

152-
Rank the missing test scenarios using these ZTWIM-specific categories.
153-
Focus **only** on scenarios NOT already covered:
152+
Select the **10 most impactful** test scenarios **NOT already covered** by
153+
existing specs (confirmed via Step 5 dedup). Rank using these categories:
154154

155155
| # | Category | What to test |
156156
|---|---|---|
157-
| 1 | **Core SPIFFE/SPIRE functionality** | Workload attestation, SVID issuance, trust bundle distribution, CSI volume mount, ClusterSPIFFEID |
158-
| 2 | **Operand lifecycle** | CR create/update/delete for each of the 4 operands, condition checks (Ready, all sub-conditions), aggregate status on ZeroTrustWorkloadIdentityManager |
159-
| 3 | **Security hardening** | SCC field validation (AllowHostNetwork, AllowPrivilegeEscalation, etc.), pod SecurityContext (runAsNonRoot, drop ALL, readOnlyRootFilesystem), restricted-v2 SCC compliance |
160-
| 4 | **Configuration edge cases** | Invalid CRs (missing required fields, out-of-range TTL, invalid persistence size), webhook rejection, boundary values |
161-
| 5 | **Reconciliation and self-healing** | Pod deletion recovery (operator, server, agent, CSI), ConfigMap drift correction, CreateOnlyMode pause/resume |
162-
| 6 | **OLM integration** | Subscription-based install, upgrade path (channel switching), uninstall cleanup, OperatorCondition.Upgradeable transition on pod failure |
163-
| 7 | **Multi-operand coordination** | ZeroTrustWorkloadIdentityManager aggregate status, cross-operand dependency (Agent needs Server running), concurrent pod failures across operands |
164-
| 8 | **OpenShift platform integration** | SCC objects on cluster, RBAC ClusterRoles, ServiceMonitor (if applicable), audit logging, version compatibility (min 2 OCP versions) |
165-
| 9 | **Error handling and negative cases** | Invalid input rejection by webhooks, permission denial, missing CRDs, operator behavior when ZeroTrustWorkloadIdentityManager CR is missing |
166-
| 10 | **Operational day-2** | Log level changes (Subscription env + CR spec), resource limits/requests, node scheduling (nodeSelector, tolerations, affinity), custom labels propagation |
167-
168-
For each PR/Jira, identify which categories are impacted. Generate test
169-
cases for the impacted categories first, then fill gaps in un-covered
170-
categories if the change is broad.
157+
| 1 | **Core Functionality** | Primary use cases: workload attestation, SVID issuance, trust bundle distribution, CSI volume mount, ClusterSPIFFEID, operand CR lifecycle |
158+
| 2 | **Configuration Edge Cases** | Invalid/boundary configurations: missing required fields, out-of-range TTL, invalid persistence size, webhook rejection, min/max boundary values |
159+
| 3 | **Dynamic Behavior** | Runtime changes not tested: log-level reload, resource limit changes, CreateOnlyMode toggle, ConfigMap drift correction, rolling update tracking |
160+
| 4 | **Integration Gaps** | Component interactions not validated: cross-operand dependency (Agent needs Server), ZeroTrustWorkloadIdentityManager aggregate status, OLM Subscription propagation |
161+
| 5 | **Multi-tenant / Namespace** | Cross-namespace scenarios: ClusterSPIFFEID with namespace selectors, workload attestation across namespaces, RBAC scoping per namespace |
162+
| 6 | **Error Handling** | Failure modes not covered: permission denial, missing CRDs, operator behavior when parent CR is absent, pod crash recovery, cascading failures |
163+
| 7 | **Upgrade / Compatibility** | Version compatibility gaps: OLM upgrade path (channel switching), CSV replacement chain, uninstall cleanup, OperatorCondition.Upgradeable transitions |
164+
| 8 | **Performance** | Load/scale testing if missing: large number of ClusterSPIFFEIDs, concurrent attestation, DaemonSet rollout on many-node clusters |
165+
| 9 | **Security** | Permission/isolation tests: SCC field validation, pod SecurityContext (runAsNonRoot, drop ALL, readOnlyRootFilesystem), restricted-v2 SCC compliance, RBAC least-privilege |
166+
| 10 | **Real Customer Scenarios** | Use cases from the RFE/Jira not tested: end-to-end workflows described in the ticket, production-like topologies, day-2 operational patterns |
167+
168+
**Prioritization rules:**
169+
170+
- For each PR/Jira, identify which categories are impacted by the diff.
171+
- Generate test cases for impacted categories **first**, then fill gaps in
172+
un-covered categories if the change is broad.
173+
- Assign a priority to each test case:
174+
- **Critical** -- blocks core functionality or security; must pass before merge.
175+
- **High** -- important gap in coverage; should be addressed in the same release.
176+
- **Medium** -- nice-to-have hardening; can be deferred if time-constrained.
177+
178+
**Test case ID format:**
179+
180+
Use `<TICKET>-TC-NNN` where `<TICKET>` is the source identifier:
181+
182+
| Source | ID example |
183+
|---|---|
184+
| Jira `SPIRE-439` | `SPIRE-439-TC-001` |
185+
| Jira `OCPSTRAT-1234` | `OCPSTRAT-1234-TC-001` |
186+
| GitHub PR #105 | `PR-105-TC-001` |
187+
188+
Number sequentially (`TC-001`, `TC-002`, ...) within a single test plan.
171189

172190
### Step 7: Generate test-cases.md
173191

174-
Write the test plan to a local output directory:
192+
Write the test plan to a local output directory. Use the Jira key when
193+
available, otherwise use the PR number:
175194

176195
```bash
177-
mkdir -p output/${JIRA_KEY:-pr-$PR_NUMBER}
196+
# Jira source
197+
mkdir -p output/${JIRA_KEY}
198+
# GitHub PR source
199+
mkdir -p output/pr-${PR_NUMBER}
178200
```
179201

180-
Use this template for `output/<key>/test-cases.md`:
202+
File name: `test-cases.md` (e.g. `output/SPIRE-439/test-cases.md` or
203+
`output/pr-105/test-cases.md`).
204+
205+
Use this template:
181206

182207
```markdown
183208
# Test Plan: <title>
@@ -191,19 +216,22 @@ Use this template for `output/<key>/test-cases.md`:
191216

192217
## Test Cases
193218

194-
### TC-001: <Title> [P0]
195-
**Domain:** <domain-key>
196-
**Category:** <one of the 10 categories above>
197-
**OpenShift-specific:** yes/no
198-
**Scope:** <keywords>
199-
**Prerequisites:** <cluster state, CRDs, namespaces>
219+
### <TICKET>-TC-001: <Title>
220+
**Priority:** Critical | High | Medium
221+
**Domain:** <domain-key(s) from Section 6>
222+
**Category:** <one of the 10 categories from Step 6>
223+
**OpenShift-specific:** yes / no
224+
**Coverage Gap:** <What existing tests do NOT cover that this test fills.>
225+
**Prerequisites:** <cluster state, CRDs, namespaces, operator version>
200226
**Steps:**
201-
1. ...
202-
2. ...
203-
**Expected result:** ...
227+
1. <Concrete action with actual config/commands>
228+
**Expected:** <What should happen after this step>
229+
2. <Next action>
230+
**Expected:** <Outcome>
231+
3. ...
204232
**Stop condition:** <which later TCs are blocked if this fails>
205233

206-
(repeat for TC-002, TC-003, ...)
234+
(repeat for <TICKET>-TC-002, <TICKET>-TC-003, ... up to 10)
207235

208236
## Coverage Map
209237

@@ -241,6 +269,17 @@ Use this template for `output/<key>/test-cases.md`:
241269
Mark each checklist item `[x]` if a TC covers it. Append a warning for
242270
missing items.
243271

272+
**Writing good test steps:**
273+
274+
- Each step must be **concrete**: include the actual API call, kubectl
275+
command, CR YAML snippet, or Go assertion -- not vague prose.
276+
- Every step must have its own **Expected** line describing the observable
277+
outcome (HTTP status, condition value, field value, pod state, etc.).
278+
- If a step creates a resource, note the cleanup requirement
279+
(`DeferCleanup` in the eventual e2e code).
280+
- Group related assertions into one step when they all verify the same
281+
object (e.g. "Verify SCC fields" with multiple Expected sub-bullets).
282+
244283
**Default behavior stops here.** Steps 8-9 below run only if the user
245284
explicitly asks to generate e2e code or raise a PR.
246285

output/pr-105/test-cases.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Test Plan: SCC Hardening for Spire Agent and CSI Driver
2+
3+
<!-- Source: https://github.com/openshift/zero-trust-workload-identity-manager/pull/105 -->
4+
<!-- Repo: openshift/zero-trust-workload-identity-manager -->
5+
<!-- Framework: controller-runtime (operator-sdk) | Ginkgo v2 -->
6+
7+
## Summary
8+
9+
PR #105 hardens the security posture of the SPIRE Agent by disabling host
10+
networking, removing privileged mode, enforcing read-only root filesystem,
11+
dropping all Linux capabilities, and updating the custom
12+
SecurityContextConstraints to restrict host access and privilege escalation.
13+
E2E tests must verify that these hardened settings are applied to the SCC
14+
object, the DaemonSet spec, and the running pods on a live OpenShift cluster.
15+
16+
## Test Cases
17+
18+
### PR-105-TC-001: SPIRE Agent SCC field validation after hardening
19+
**Priority:** Critical
20+
**Domain:** openshift-scc, security-context
21+
**Category:** Security
22+
**OpenShift-specific:** yes
23+
**Coverage Gap:** Existing e2e only checks the `SecurityContextConstraintsAvailable` condition (boolean). No test fetches the actual `spire-agent` SCC object to verify individual field values like AllowHostNetwork, AllowPrivilegeEscalation, or RequiredDropCapabilities.
24+
**Prerequisites:** Operator installed via OLM, SpireAgent CR `cluster` created, `SecurityContextConstraintsAvailable` condition is True
25+
**Steps:**
26+
1. `k8sClient.Get(ctx, client.ObjectKey{Name: "spire-agent"}, scc)` -- fetch the SCC object from the cluster
27+
**Expected:** SCC object exists and is retrievable without error
28+
2. Assert SCC host-access fields:
29+
- `scc.AllowHostNetwork == false`
30+
- `scc.AllowHostPorts == false`
31+
- `scc.AllowPrivilegedContainer == false`
32+
- `*scc.AllowPrivilegeEscalation == false`
33+
**Expected:** All four fields are restrictive (false)
34+
3. Assert SCC retains required host access:
35+
- `scc.AllowHostPID == true`
36+
- `scc.AllowHostDirVolumePlugin == true`
37+
**Expected:** Both remain true (required for node attestation and socket dir)
38+
4. Assert SCC filesystem and capability restrictions:
39+
- `scc.ReadOnlyRootFilesystem == true`
40+
- `scc.RequiredDropCapabilities` contains `"ALL"`
41+
**Expected:** Filesystem is read-only and all capabilities are dropped
42+
5. Assert SCC RunAsUser strategy:
43+
- `scc.RunAsUser.Type == RunAsUserStrategyRunAsAny`
44+
**Expected:** RunAsUser is RunAsAny
45+
**Stop condition:** PR-105-TC-002 depends on the SCC being correctly applied (SCC governs pod admission)
46+
47+
### PR-105-TC-002: SPIRE Agent DaemonSet and pod security context verification
48+
**Priority:** Critical
49+
**Domain:** security-context, controller-manager
50+
**Category:** Security
51+
**OpenShift-specific:** no
52+
**Coverage Gap:** Existing e2e waits for `DaemonSetAvailable` (replica count) but never inspects the DaemonSet pod template or running pod security settings. No test verifies HostNetwork, HostPID, DNSPolicy, Privileged, AllowPrivilegeEscalation, ReadOnlyRootFilesystem, or Capabilities.Drop on the spire-agent workload.
53+
**Prerequisites:** Operator installed via OLM, SpireAgent CR `cluster` created, DaemonSet `spire-agent` is available with all pods running
54+
**Steps:**
55+
1. `clientset.AppsV1().DaemonSets(ns).Get(ctx, "spire-agent", ...)` -- fetch the DaemonSet
56+
**Expected:** DaemonSet exists and is retrievable without error
57+
2. Assert DaemonSet pod template spec:
58+
- `podSpec.HostNetwork == false`
59+
- `podSpec.HostPID == true`
60+
- `podSpec.DNSPolicy == corev1.DNSClusterFirst`
61+
**Expected:** Host networking disabled, host PID retained, DNS policy matches non-host-network mode
62+
3. Find the `spire-agent` container in the pod template and assert its SecurityContext:
63+
- `sc.Privileged == ptr.To(false)`
64+
- `sc.AllowPrivilegeEscalation == ptr.To(false)`
65+
- `sc.ReadOnlyRootFilesystem == ptr.To(true)`
66+
- `sc.Capabilities.Drop` contains `"ALL"`
67+
**Expected:** Container runs unprivileged with minimal capabilities
68+
4. `clientset.CoreV1().Pods(ns).List(ctx, {LabelSelector: "app.kubernetes.io/name=spire-agent"})` -- list running pods
69+
**Expected:** At least one pod exists (DaemonSet should schedule one per node)
70+
5. For each running pod, assert the same fields from steps 2-3 on the live pod spec
71+
**Expected:** All running pods reflect the hardened template (no drift between template and actual)
72+
**Stop condition:** Attestation tests are blocked if spire-agent pods fail to start under the hardened security context
73+
74+
## Coverage Map
75+
76+
| Scenario | Existing spec | Domain | Decision |
77+
|---|---|---|---|
78+
| SCC AllowHostNetwork/Ports/Privileged fields | (none) | openshift-scc | new-in-file |
79+
| SCC AllowPrivilegeEscalation field | (none) | openshift-scc | new-in-file |
80+
| SCC ReadOnlyRootFilesystem field | (none) | openshift-scc | new-in-file |
81+
| SCC RequiredDropCapabilities field | (none) | openshift-scc | new-in-file |
82+
| SCC RunAsUser strategy | (none) | openshift-scc | new-in-file |
83+
| DaemonSet HostNetwork=false, DNSPolicy | (none) | security-context | new-in-file |
84+
| DaemonSet container SecurityContext | (none) | security-context | new-in-file |
85+
| Running pod security context | (none) | security-context | new-in-file |
86+
| SecurityContextConstraintsAvailable condition | test/e2e/e2e_test.go:246 | openshift-scc | skip (already covered) |
87+
| DaemonSet available after install | test/e2e/e2e_test.go:252 | controller-manager | skip (already covered) |
88+
89+
## OLM Coverage
90+
- Subscription install: covered (existing Installation context)
91+
- Channel switching: not covered
92+
- Upgrade path: not covered
93+
- Dependency management: not covered
94+
- Uninstall cleanup: not covered
95+
96+
## OpenShift Coverage
97+
- SCC validation: **covered (PR-105-TC-001)**
98+
- RBAC scoping: covered (existing)
99+
- Image scanning: not covered
100+
- Prometheus metrics: not covered
101+
- Audit logging: not covered
102+
- Version compatibility: not covered
103+
104+
## Red Hat Certification Checklist
105+
- [x] OLM install
106+
- [x] SCC validation
107+
- [x] RBAC least-privilege
108+
- [ ] Image scanning / signing
109+
- [ ] Prometheus metrics
110+
- [ ] Audit logging
111+
- [ ] Version compatibility
112+
- [ ] Uninstall cleanup
113+
- [x] Security context

test/e2e/e2e_suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
. "github.com/onsi/ginkgo/v2"
2525
. "github.com/onsi/gomega"
26+
securityv1 "github.com/openshift/api/security/v1"
2627
configv1 "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
2728
operatorv1alpha1 "github.com/openshift/zero-trust-workload-identity-manager/api/v1alpha1"
2829
"github.com/openshift/zero-trust-workload-identity-manager/test/e2e/utils"
@@ -58,6 +59,7 @@ var _ = BeforeSuite(func() {
5859
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
5960
utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
6061
utilruntime.Must(operatorv1.AddToScheme(scheme))
62+
utilruntime.Must(securityv1.AddToScheme(scheme))
6163
utilruntime.Must(spiffev1alpha1.AddToScheme(scheme))
6264

6365
// Create controller-runtime client

0 commit comments

Comments
 (0)