R-1285: after every bundle install the agent asks the wrapper now on disk for the appliance-only policy pass, so a BYO host upgraded from an older wrapper loses our SSH drop-in in one update. Red-proved. Commit only, not released.
gates / gates (push) Successful in 1m42s
gates / gates (push) Successful in 1m42s
This commit is contained in:
@@ -1,3 +1,18 @@
|
||||
## 2026-10-25 (night, unattended) — a BYO host loses our SSH drop-in in ONE update (R-1285) — committed, NOT released
|
||||
|
||||
- **The cause, measured 2026-10-25 on a BYO test host:** the bundle that brings a new `felhom-os-apply` is planned by
|
||||
the OLD one on disk, which did not know D43's appliance-only policy — so a BYO host upgraded from 0.164.0 kept our
|
||||
drop-in until its next bundle. A change inside the wrapper alone cannot fix the run that delivers it.
|
||||
- **The fix — the new AGENT asks the new wrapper:** `internal/osupdate/bundle.go` runs a policy pass right after every
|
||||
bundle install (`mode: "bundle-policy"`, host layer, no envelope); the wrapper on disk is the bundle's own by then.
|
||||
`configs/felhom-os-apply` `Bundle.policy_pass`: on a root-owned BYO record it removes the appliance-only files WE
|
||||
wrote (our header), keeps the previous copy beside the bundle's, `sshd -t` then reloads ssh — a failing `sshd -t` puts
|
||||
the file back. It writes nothing, so it needs no signature; an appliance and the owner's own file are left alone. Never
|
||||
fatal for the agent: an older wrapper refuses the unknown mode and the next bundle does it. Report key `bundle_policy`.
|
||||
- Tests: `TestR1285_AnInstalledBundleIsFollowedByThePolicyPass` (a refused bundle runs no pass);
|
||||
`ByoKeepsItsSSH.test_the_policy_pass_*` (three). Red-proofs: felhom.eu
|
||||
`documentation/audits/night-2026-10-25/redproof-r1285.txt`.
|
||||
|
||||
## 2026-10-25 (night, unattended) — a signed job's window is at most 24 h (R-1280, D44 #22) — committed, NOT released (rides the next agent release; the vouch is the operator's)
|
||||
|
||||
- `internal/authz/verifier.go`: `MaxWindow = 24h`; a blob whose `expires_at - issued_at` is longer is refused
|
||||
|
||||
+44
-1
@@ -491,7 +491,8 @@ class Apply:
|
||||
|
||||
def check_plan(self, plan):
|
||||
mode = plan.get("mode", "apply")
|
||||
if mode not in ("apply", "inventory", "health", "facts", "live-restore-on", "bundle", "agent_update", "oom-check") \
|
||||
if mode not in ("apply", "inventory", "health", "facts", "live-restore-on", "bundle", "bundle-policy", "agent_update",
|
||||
"oom-check") \
|
||||
+ KERNEL_MODES:
|
||||
raise Refused("R11", f"unknown mode {mode!r}")
|
||||
if mode == "agent_update":
|
||||
@@ -502,6 +503,10 @@ class Apply:
|
||||
if plan.get("layer") != "host":
|
||||
raise Refused("R11", "bundle is a host-layer mode")
|
||||
return mode, "host", 0, "bundle"
|
||||
if mode == "bundle-policy":
|
||||
if plan.get("layer") != "host":
|
||||
raise Refused("R11", "bundle-policy is a host-layer mode")
|
||||
return mode, "host", 0, "bundle-policy"
|
||||
layer = plan.get("layer")
|
||||
if layer not in ("guest", "host", "docker", "pve", "kernel"):
|
||||
raise Refused("R12", f"layer {layer!r} is not guest, host, docker, pve or kernel")
|
||||
@@ -1074,6 +1079,8 @@ class Apply:
|
||||
return self.facts()
|
||||
if self.mode == "bundle":
|
||||
return Bundle(self).from_plan(plan)
|
||||
if self.mode == "bundle-policy":
|
||||
return Bundle(self).policy_pass()
|
||||
if self.mode == "agent_update":
|
||||
return self.agent_update(plan)
|
||||
if self.layer in ("host", "pve", "kernel"):
|
||||
@@ -2064,6 +2071,42 @@ class Bundle:
|
||||
ver, files = self.parse(data, sha)
|
||||
return self.install(ver, sha, files, "installer", False)
|
||||
|
||||
def policy_pass(self):
|
||||
"""R-1285 (D43 = A): the appliance-only files' policy, applied by THIS wrapper. A bundle that brings a new wrapper
|
||||
is planned by the OLD one, which did not know the policy — so on a BYO host upgraded from 0.164.0 our SSH drop-in
|
||||
stayed until the next bundle. The agent runs this pass right after every bundle install (the wrapper on disk is
|
||||
then the new one). It only REMOVES a file WE wrote (our header) from a box whose root-owned install record says
|
||||
byo, keeps the previous copy beside the bundle's, and reloads sshd; it writes nothing, so it needs no signature.
|
||||
A failing `sshd -t` puts the file back (R-1257 X4's rule). Report key `bundle_policy`."""
|
||||
log = self.r.log
|
||||
install_mode = self.install_mode()
|
||||
removed = []
|
||||
if install_mode == "byo":
|
||||
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(self.r.now()))
|
||||
prev = os.path.join(BUNDLE_PREV_DIR, f"{stamp}-policy")
|
||||
for dest, _src, _mode, _check, policy in BUNDLE_FILES:
|
||||
if policy != "appliance":
|
||||
continue
|
||||
old, old_mode, _ = self.live(dest)
|
||||
if old is None or not old.startswith(SSHD_DROPIN_HEADER):
|
||||
continue
|
||||
self.r.put_file(prev + dest, old, 0o600)
|
||||
self.r.remove(dest)
|
||||
removed.append((dest, old, old_mode))
|
||||
log(f"os-apply: BUNDLE POLICY REMOVED {dest} (ours, on a BYO host — D43, R-1285)")
|
||||
if any(d == SSHD_DROPIN for d, _, _ in removed):
|
||||
rc, out, err = self.r.host(["/usr/sbin/sshd", "-t"], 120)
|
||||
if rc != 0:
|
||||
for d, old, old_mode in removed:
|
||||
self.r.put_file(d, old, old_mode)
|
||||
log(f"os-apply: BUNDLE POLICY WARNING sshd -t failed without our drop-in (rc={rc}) — put back")
|
||||
removed = []
|
||||
else:
|
||||
self.must(["systemctl", "try-reload-or-restart", "ssh"], "systemctl try-reload-or-restart ssh")
|
||||
self.report["bundle_policy"] = {"install_mode": install_mode, "removed": [d for d, _, _ in removed]}
|
||||
log(f"os-apply: BUNDLE POLICY DONE install_mode={install_mode} removed={len(removed)}")
|
||||
return 0
|
||||
|
||||
# ---------- install, self-check, undo ----------
|
||||
def install(self, ver, sha, files, authority, bootstrap):
|
||||
log = self.r.log
|
||||
|
||||
@@ -588,6 +588,47 @@ class ByoKeepsItsSSH(unittest.TestCase):
|
||||
return orig(argv, timeout, stdin)
|
||||
box.host = host
|
||||
|
||||
# R-1285: the policy pass the agent runs right after every bundle install, with the wrapper NOW on disk. No
|
||||
# signature (it only removes a file we wrote). RED-PROOF: make policy_pass return before the loop → our copy stays.
|
||||
POLICY_PLAN = {"release_id": "bundle-policy-0.166.0", "layer": "host", "mode": "bundle-policy"}
|
||||
|
||||
def test_the_policy_pass_removes_our_copy_from_a_byo_host(self):
|
||||
data, sha = real_bundle()
|
||||
box = Box(data, None, install_mode="byo")
|
||||
box.put(PLAN, json.dumps(self.POLICY_PLAN).encode(), 0o600, uid=999)
|
||||
box.put(self.DEST, self.OURS, 0o644) # the old wrapper's bundle run left it
|
||||
rc, rep = run(box)
|
||||
self.assertEqual(rc, 0, rep)
|
||||
self.assertNotIn(self.DEST, box.files)
|
||||
self.assertEqual(rep["bundle_policy"], {"install_mode": "byo", "removed": [self.DEST]}, rep)
|
||||
self.assertTrue(any(k.endswith(self.DEST) and k.startswith(osapply.BUNDLE_PREV_DIR) for k in box.files))
|
||||
self.assertLess(box.calls.index(["/usr/sbin/sshd", "-t"]), box.calls.index(["systemctl", "try-reload-or-restart", "ssh"]))
|
||||
|
||||
def test_the_policy_pass_leaves_an_appliance_and_the_owners_file(self):
|
||||
for mode, content in (("appliance", None), ("byo", b"# my own\nPermitRootLogin yes\n")):
|
||||
data, sha = real_bundle()
|
||||
box = Box(data, None, install_mode=mode)
|
||||
box.put(PLAN, json.dumps(self.POLICY_PLAN).encode(), 0o600, uid=999)
|
||||
body = content if content is not None else self.OURS
|
||||
box.put(self.DEST, body, 0o644)
|
||||
rc, rep = run(box)
|
||||
self.assertEqual(rc, 0, rep)
|
||||
self.assertEqual(box.files.get(self.DEST), body, mode)
|
||||
self.assertEqual(rep["bundle_policy"]["removed"], [], mode)
|
||||
self.assertNotIn(["systemctl", "try-reload-or-restart", "ssh"], box.calls, mode)
|
||||
|
||||
def test_the_policy_pass_puts_our_copy_back_when_sshd_t_fails(self):
|
||||
data, sha = real_bundle()
|
||||
box = Box(data, None, install_mode="byo")
|
||||
box.put(PLAN, json.dumps(self.POLICY_PLAN).encode(), 0o600, uid=999)
|
||||
box.put(self.DEST, self.OURS, 0o644)
|
||||
self._fake(box, sshd_t_rc=1)
|
||||
rc, rep = run(box)
|
||||
self.assertEqual(rc, 0, rep)
|
||||
self.assertEqual(box.files.get(self.DEST), self.OURS)
|
||||
self.assertEqual(rep["bundle_policy"]["removed"], [])
|
||||
self.assertNotIn(["systemctl", "try-reload-or-restart", "ssh"], box.calls)
|
||||
|
||||
def test_a_match_block_for_the_lan_is_not_in_force(self):
|
||||
data, sha = real_bundle()
|
||||
box = Box(data, signed_job(sha))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -114,12 +115,30 @@ func (e ConfigUpdateExecutor) Execute(ctx context.Context, op string, params jso
|
||||
return fmt.Errorf("agent_config_update: failed (previous files restored): %s", wr.Failed)
|
||||
}
|
||||
lg.Warn("osupdate: config bundle INSTALLED", "bundle", string(wr.Bundle), "pass_seconds", wr.PassSeconds)
|
||||
e.policyPass(ctx, lg, p.AgentVersion)
|
||||
if e.AfterInstall != nil {
|
||||
e.AfterInstall(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// policyPass (R-1285) asks the wrapper NOW on disk — after this install, the bundle's own — to apply the
|
||||
// appliance-only files' policy (mode "bundle-policy"): on a BYO host it removes the SSH drop-in we wrote. The bundle run
|
||||
// itself was planned by the wrapper that was on disk BEFORE it, which may predate the policy (0.164.0). Never fatal:
|
||||
// the bundle is installed either way; an older wrapper refuses the unknown mode and the next bundle does it.
|
||||
func (e ConfigUpdateExecutor) policyPass(ctx context.Context, lg *slog.Logger, ver string) {
|
||||
runID := "bundle-policy-" + e.Leg.now().UTC().Format("20060102T150405Z")
|
||||
wr, err := e.Leg.call(ctx, runID, map[string]any{"release_id": "bundle-policy-" + ver, "layer": LayerHost, "mode": "bundle-policy"})
|
||||
switch {
|
||||
case err != nil:
|
||||
lg.Warn("osupdate: the bundle policy pass did not run", "err", err)
|
||||
case wr.refused():
|
||||
lg.Info("osupdate: the bundle policy pass was refused (a wrapper before 0.166.0?) — the next bundle applies it", "refused", string(wr.Refused))
|
||||
default:
|
||||
lg.Info("osupdate: bundle policy pass done", "bundle_policy", string(wr.BundlePolicy))
|
||||
}
|
||||
}
|
||||
|
||||
func (e ConfigUpdateExecutor) download(ctx context.Context, url, dest string) (string, error) {
|
||||
hc := e.HTTPClient
|
||||
if hc == nil {
|
||||
|
||||
@@ -38,6 +38,9 @@ func (b *bundleWrapper) Run(_ context.Context, name string, args ...string) ([]b
|
||||
var plan map[string]any
|
||||
_ = json.Unmarshal(raw, &plan)
|
||||
b.plans = append(b.plans, plan)
|
||||
if plan["mode"] == "bundle-policy" { // R-1285: the pass after an install carries no bundle
|
||||
return []byte(`OSAPPLY-REPORT {"mode":"bundle-policy","bundle_policy":{"install_mode":"byo","removed":[]}}` + "\n"), nil, nil
|
||||
}
|
||||
body, _ := os.ReadFile(plan["bundle"].(string))
|
||||
b.bodies = append(b.bodies, body)
|
||||
return []byte("OSAPPLY-REPORT " + b.rep + "\n"), nil, nil
|
||||
@@ -153,3 +156,27 @@ func TestBundleURL(t *testing.T) {
|
||||
func (b *bundleWrapper) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
|
||||
return b.Run(ctx, name, args...)
|
||||
}
|
||||
|
||||
// R-1285: right after an install the agent asks the wrapper now on disk for the appliance-only policy pass — the bundle
|
||||
// run itself was planned by the OLD wrapper (on a BYO host upgraded from 0.164.0 our SSH drop-in stayed one bundle
|
||||
// longer). A refused or failed bundle runs no pass.
|
||||
// RED-PROOF: drop the policyPass call → one plan only.
|
||||
func TestR1285_AnInstalledBundleIsFollowedByThePolicyPass(t *testing.T) {
|
||||
body := []byte(`{"format":1,"agent_version":"0.143.0","files":[]}`)
|
||||
srv, sha := serveBundle(t, body)
|
||||
w := &bundleWrapper{t: t, rep: `{"mode":"bundle","bundle":{"agent_version":"0.143.0","written":["/usr/local/sbin/felhom-os-apply"]}}`}
|
||||
e, _ := bundleExec(t, w, srv.URL)
|
||||
if err := e.Execute(signedCtx(), OpConfigUpdate, params("0.143.0", sha)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(w.plans) != 2 || w.plans[1]["mode"] != "bundle-policy" || w.plans[1]["layer"] != "host" || w.plans[1]["signed"] != nil {
|
||||
t.Fatalf("after the install the agent must run the policy pass (host layer, no envelope): %v", w.plans)
|
||||
}
|
||||
|
||||
w2 := &bundleWrapper{t: t, rep: `{"mode":"bundle","refused":{"code":"R3","reason":"x"}}`}
|
||||
e2, _ := bundleExec(t, w2, srv.URL)
|
||||
_ = e2.Execute(signedCtx(), OpConfigUpdate, params("0.143.0", sha))
|
||||
if len(w2.plans) != 1 {
|
||||
t.Fatalf("a refused bundle must run no policy pass: %v", w2.plans)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,8 @@ type WrapperReport struct {
|
||||
Undo bool `json:"undo"`
|
||||
LiveRestore json.RawMessage `json:"live_restore"`
|
||||
Facts json.RawMessage `json:"facts"`
|
||||
Bundle json.RawMessage `json:"bundle"` // the config bundle's result (R-840, mode "bundle")
|
||||
Bundle json.RawMessage `json:"bundle"` // the config bundle's result (R-840, mode "bundle")
|
||||
BundlePolicy json.RawMessage `json:"bundle_policy"` // the appliance-only policy pass (R-1285, mode "bundle-policy")
|
||||
// OOMCheck (R-528, `09` decision 157): the docker layer's memory-kill check, {result, oom_killed, oom_event,
|
||||
// exit_code, image, detail}. Carried to the hub UNCHANGED; the agent never reads it.
|
||||
OOMCheck json.RawMessage `json:"oom_check"`
|
||||
|
||||
Reference in New Issue
Block a user