Skip to content

Commit f139379

Browse files
fix(packaging): pin Kensa rollback store inside the writable state tree (PKG-3) (#673)
The hardened systemd unit sets ProtectSystem=strict with ReadWritePaths=/var/lib/openwatch but set no WorkingDirectory and never set OPENWATCH_KENSA_STORE_PATH. So kensaStorePath() fell back to .kensa/remediation.db relative to the working dir, which systemd defaults to the read-only /. Kensa's OpenSQLite MkdirAll then failed, the remediation service failed to compose at boot, and every remediation/rollback returned 'kensa: remediate path not wired' while scans (store-less engine) kept working. Set OPENWATCH_KENSA_STORE_PATH=/var/lib/openwatch/kensa/remediation.db (inside ReadWritePaths) and WorkingDirectory=/var/lib/openwatch as defense-in-depth. Spec C-13/AC-23 + a source-inspection regression test backstop it. Fixes remediation on every hardened packaged install (regressed by rc.14).
1 parent b0db58b commit f139379

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

packaging/common/openwatch.service

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ Type=simple
99
User=openwatch
1010
Group=openwatch
1111
EnvironmentFile=-/etc/openwatch/secrets.env
12+
# Kensa remediation needs a durable SQLite store for rollback pre-state. Pin it
13+
# inside the writable state tree: ProtectSystem=strict makes / read-only, and
14+
# without this the path defaults to `.kensa/remediation.db` under the working
15+
# dir (systemd defaults that to read-only /), so the store fails to open and
16+
# remediation silently degrades to "not wired" while scans still work.
17+
Environment=OPENWATCH_KENSA_STORE_PATH=/var/lib/openwatch/kensa/remediation.db
18+
WorkingDirectory=/var/lib/openwatch
1219
ExecStart=/usr/bin/openwatch serve --config /etc/openwatch/openwatch.toml
1320
Restart=on-failure
1421
RestartSec=5s

packaging/tests/package_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,49 @@ func TestOpenwatch_DependsOnKensaRules(t *testing.T) {
504504
})
505505
}
506506

507+
// @ac AC-23
508+
// AC-23: the hardened unit pins the Kensa rollback store inside the writable
509+
// state tree. Backstops the rc.14 regression where ProtectSystem=strict left
510+
// the store path on the read-only root, so remediation boot-wiring failed and
511+
// every remediation/rollback returned "not wired" while scans still worked.
512+
func TestUnit_KensaStorePathIsWritable(t *testing.T) {
513+
t.Run("release-package-build/AC-23", func(t *testing.T) {
514+
dir := appDir(t)
515+
unit, err := os.ReadFile(filepath.Join(dir, "packaging", "common", "openwatch.service"))
516+
if err != nil {
517+
t.Fatalf("read openwatch.service: %v", err)
518+
}
519+
u := string(unit)
520+
521+
// The store path must be set and live under /var/lib/openwatch, which is
522+
// the unit's ReadWritePaths entry (so it is writable under
523+
// ProtectSystem=strict).
524+
storeRe := regexp.MustCompile(`(?m)^Environment=OPENWATCH_KENSA_STORE_PATH=(\S+)`)
525+
m := storeRe.FindStringSubmatch(u)
526+
if m == nil {
527+
t.Fatal("openwatch.service must set Environment=OPENWATCH_KENSA_STORE_PATH (else the Kensa rollback store lands on the read-only root and remediation degrades to \"not wired\")")
528+
}
529+
if !strings.HasPrefix(m[1], "/var/lib/openwatch/") {
530+
t.Errorf("OPENWATCH_KENSA_STORE_PATH=%q must be under /var/lib/openwatch/ (a ReadWritePaths entry)", m[1])
531+
}
532+
533+
// Defense in depth: WorkingDirectory must be writable too, so the store's
534+
// default fallback (.kensa/ relative to the working dir) cannot land on
535+
// the read-only root if the env var is ever dropped.
536+
wdRe := regexp.MustCompile(`(?m)^WorkingDirectory=(\S+)`)
537+
if wm := wdRe.FindStringSubmatch(u); wm == nil {
538+
t.Error("openwatch.service must set WorkingDirectory to a writable path")
539+
} else if !strings.HasPrefix(wm[1], "/var/lib/openwatch") {
540+
t.Errorf("WorkingDirectory=%q must be under /var/lib/openwatch", wm[1])
541+
}
542+
543+
// Sanity: the writable path we point at is actually granted by the unit.
544+
if !strings.Contains(u, "ReadWritePaths=") || !strings.Contains(u, "/var/lib/openwatch") {
545+
t.Error("openwatch.service must grant ReadWritePaths=/var/lib/openwatch")
546+
}
547+
})
548+
}
549+
507550
// @ac AC-17
508551
// AC-17: make packages builds the kensa-rules corpus package too.
509552
func TestMake_PackagesBuildsKensaRules(t *testing.T) {

specs/release/package-build.spec.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ spec:
138138
the tilde form. Source-inspection enforces (AC-21).
139139
type: technical
140140
enforcement: error
141+
- id: C-13
142+
description: >
143+
The systemd unit MUST set OPENWATCH_KENSA_STORE_PATH to a path inside
144+
the unit's writable state tree (a ReadWritePaths entry, e.g.
145+
/var/lib/openwatch). Kensa remediation composes a full engine backed by
146+
a durable SQLite transaction store for rollback pre-state; under
147+
ProtectSystem=strict the store path MUST resolve to a writable location
148+
or the store fails to open and remediation degrades to "not wired"
149+
(scans, which compose a store-less engine, are unaffected and mask the
150+
failure). The unit MUST also set WorkingDirectory to a writable path so
151+
the store's default fallback (.kensa/ relative to the working dir) does
152+
not land on the read-only root.
153+
type: technical
154+
enforcement: error
141155

142156
acceptance_criteria:
143157
- id: AC-01
@@ -247,3 +261,14 @@ spec:
247261
removes) an operator's replacement certificate, including the transition.
248262
priority: critical
249263
references_constraints: [C-05]
264+
- id: AC-23
265+
description: >-
266+
Source-inspection of packaging/common/openwatch.service: the unit sets
267+
OPENWATCH_KENSA_STORE_PATH to a path under /var/lib/openwatch (a
268+
ReadWritePaths entry), and sets WorkingDirectory to a writable path
269+
(not the default read-only /). Backstops the rc.14 regression where the
270+
hardened unit left the Kensa rollback store unwritable, so remediation
271+
boot-wiring failed ("compose remediation service") and every
272+
remediation/rollback returned "not wired" while scans worked.
273+
priority: critical
274+
references_constraints: [C-13]

0 commit comments

Comments
 (0)