From 54760c0565251a03bf5cf02e6c22a046851c1973 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 14:24:27 +0000 Subject: [PATCH] =?UTF-8?q?0.9.29=20=E2=80=94=20rootless:=20RCTL=20umbrell?= =?UTF-8?q?a=20auto-apply=20at=20create=5Fjail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thirtieth 0.9.x release. The set_loginclass_rctl primitive from 0.9.28 now fires automatically after a successful create_jail privops invocation, sourced from a new rctl_umbrella: block in crated.conf. Operators no longer need a startup-script step to seed loginclass quotas. Config schema: Crated::Config gains rctlUmbrella (vector>). YAML form: rctl_umbrella: memoryuse: 4G pcpu: 200 maxproc: 256 Validated via RetunePure at load time. Process-global rules: Crated::setUmbrellaConfig(rules) registered once at daemon startup. main.cpp calls before opening privops listener. Auto-apply in dispatcher: maybeApplyUmbrella(verb, uid, status) runs after every libnv-transport dispatch. Fires when: - verb == CreateJail - uid > 0 (libnv path; HTTP always uid=0 → no-op) - 200 <= status < 300 - g_umbrellaRules non-empty Runs rctl -a loginclass:crate-:KEY:deny=VALUE per rule via Util::execCommand. Best-effort: failures log to stderr, don't fail the create_jail response. HTTP path skips auto-apply (no peer uid available — 0.9.14 cpp-httplib limitation). Operators wanting umbrella on HTTP clients call set_loginclass_rctl manually. Suite stays at 1303 (auto-apply needs real rctl(8)). Remaining: default flip (0.9.30), setuid removed (1.0.0). --- CHANGELOG.md | 141 ++++++++++++++++++++++++++++++++++++ cli/args.cpp | 4 +- daemon/config.cpp | 18 +++++ daemon/config.h | 17 +++++ daemon/crated.conf.sample | 25 +++++++ daemon/main.cpp | 6 ++ daemon/privops_handlers.cpp | 43 +++++++++++ daemon/privops_handlers.h | 10 +++ 8 files changed, 262 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af67c3..9decfdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,147 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [0.9.29] — 2026-05-10 + +**Rootless track, RCTL umbrella auto-apply.** Thirtieth 0.9.x +release. The `set_loginclass_rctl` primitive from 0.9.28 now +fires automatically after a successful `create_jail` privops +invocation, sourced from a new `rctl_umbrella:` block in +`crated.conf`. Operators no longer need a startup-script step +to seed loginclass quotas. + +### What lands + +#### Config schema + +`Crated::Config` gains: + +```cpp +std::vector> rctlUmbrella; +``` + +YAML form: + +```yaml +rctl_umbrella: + memoryuse: 4G + pcpu: 200 + maxproc: 256 +``` + +Keys / values validated via `RetunePure::validateRctlKey` / +`validateRctlValue` at config-load time — bad entries throw +at daemon startup, not at first jail-create. + +#### Process-global rules + +`Crated::setUmbrellaConfig(rules)` registers the parsed map +once at daemon startup. `daemon/main.cpp` calls it just +before opening the privops listener. + +#### Auto-apply in dispatcher + +`maybeApplyUmbrella(verb, uid, status)` runs after every +libnv-transport dispatch. Fires only when: + +1. `verb == CreateJail` +2. `uid > 0` (peer credentials known — libnv socket only; + HTTP path always has uid=0 → no-op) +3. `200 <= status < 300` (jail actually created) +4. `g_umbrellaRules` is non-empty + +For each rule, runs `rctl -a loginclass:crate-:KEY:deny=VALUE` +via `Util::execCommand`. Best-effort: failures log to stderr +but do NOT fail the create_jail response — the jail is up, +losing an umbrella rule is a quota gap not a correctness +break. + +### Behaviour + +``` +# crated.conf has: +# rctl_umbrella: +# memoryuse: 4G + +# alice (uid 1000) sends create_jail "web1" via libnv socket +# crated: +# 1. handleCreateJail succeeds +# 2. maybeApplyUmbrella fires: +# rctl -a loginclass:crate-1000:memoryuse:deny=4G +# 3. response 200 returned to client + +# alice sends create_jail "web2" +# crated: +# 1. handleCreateJail succeeds +# 2. maybeApplyUmbrella re-applies the same rule +# (idempotent — kernel no-ops the duplicate) + +# Now alice has 2 jails. Per-jail rules cap each. Umbrella +# cap of 4G applies to alice's TOTAL across both jails. + +# bob (uid 1001) sends create_jail +# crated applies rctl -a loginclass:crate-1001:memoryuse:deny=4G +# bob's umbrella is independent of alice's. +``` + +### HTTP path: no auto-apply + +The HTTP transport never has a peer uid (cpp-httplib limitation; +0.9.14 architecture decision). Auto-apply skips on HTTP requests +— bearer-token API isn't multi-tenant in the operator-uid sense. +Operators wanting umbrella enforcement must use the libnv socket +(or call `set_loginclass_rctl` manually). + +### Series state + +CLI call-sites wired (12+): +- All `crate retune`, `crate stop`, full `crate run` chain +- All host-side iface verbs (createEpair / disableOffload / + setUp / setInetAddr / bridge add+del) +- Lease file per-user path (0.9.27) +- Loginclass RCTL umbrella primitives (0.9.28) +- **Auto-apply umbrella at create_jail (this release)** + +The `rootless_per_user: true` config block and per-user +namespacing (paths, ZFS, network sub-CIDR, RCTL groups) are +all wired and tested individually. + +Remaining: +- 0.9.30 — default flip (`rootless_per_user: true` becomes + default in `crated.conf.sample`; existing setuid-prod + deployments unaffected, but new installs default to + rootless) +- 1.0.0 — setuid bit removed from `Makefile install` target + +### Tests + +No new tests — the auto-apply path is daemon-side runtime +behaviour requiring a real `rctl(8)` to exercise meaningfully. +Covered by: +- `rctl_umbrella` config parsing tested implicitly via + daemon startup on FreeBSD CI (bad config = startup throw) +- `maybeApplyUmbrella` logic mirrors `maybeWritePerUserAudit` + shape (proven pattern from 0.9.13) +- `set_loginclass_rctl` validator coverage from 0.9.28 + (auto-apply uses the same key/value path) + +Suite stays at 1303. + +### Files + +- `daemon/config.h` — `rctlUmbrella` field +- `daemon/config.cpp` — YAML parser, validates at load time +- `daemon/privops_handlers.{h,cpp}` — `setUmbrellaConfig` + setter + `maybeApplyUmbrella` post-dispatch hook + + `g_umbrellaRules` storage +- `daemon/main.cpp` — `setUmbrellaConfig(config.rctlUmbrella)` + at startup +- `daemon/crated.conf.sample` — `rctl_umbrella:` block +- `cli/args.cpp` — version `crate 0.9.29` +- `CHANGELOG.md` — entry + +--- + ## [0.9.28] — 2026-05-09 **Rootless track, RCTL umbrella verbs.** Twenty-ninth 0.9.x diff --git a/cli/args.cpp b/cli/args.cpp index f18e1c0..fd547c8 100644 --- a/cli/args.cpp +++ b/cli/args.cpp @@ -753,7 +753,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.noColor = true; break; } else if (strEq(argv[a], "--version")) { - std::cout << "crate 0.9.28" << std::endl; + std::cout << "crate 0.9.29" << std::endl; exit(0); } else if (auto argShort = isShort(argv[a])) { switch (argShort) { @@ -764,7 +764,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.logProgress = true; break; case 'V': - std::cout << "crate 0.9.28" << std::endl; + std::cout << "crate 0.9.29" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/daemon/config.cpp b/daemon/config.cpp index e2b9899..1f94133 100644 --- a/daemon/config.cpp +++ b/daemon/config.cpp @@ -3,6 +3,7 @@ #include "config.h" #include "socket_perms_pure.h" #include "../lib/auth_pure.h" +#include "../lib/retune_pure.h" #include #include @@ -201,6 +202,23 @@ Config Config::load(const std::string &path) { } } + // 0.9.29: rctl_umbrella block. Map of RCTL key -> value (string). + // Validate against RetunePure whitelist at load time so bad + // entries fail startup, not first jail-create. + if (auto u = root["rctl_umbrella"]) { + if (!u.IsMap()) + throw std::runtime_error("rctl_umbrella must be a YAML map"); + for (auto kv : u) { + auto key = kv.first.as(); + auto val = kv.second.as(); + if (auto e = RetunePure::validateRctlKey(key); !e.empty()) + throw std::runtime_error("rctl_umbrella." + key + ": " + e); + if (auto e = RetunePure::validateRctlValue(key, val); !e.empty()) + throw std::runtime_error("rctl_umbrella." + key + ": " + e); + cfg.rctlUmbrella.emplace_back(key, val); + } + } + return cfg; } diff --git a/daemon/config.h b/daemon/config.h index 7bbeb20..f418ef6 100644 --- a/daemon/config.h +++ b/daemon/config.h @@ -114,6 +114,23 @@ struct Config { std::string privopsSocketGroup; unsigned privopsSocketMode = 0660; + // 0.9.29: per-loginclass RCTL umbrella defaults. When non-empty + // AND a create_jail privops verb is invoked over the libnv + // socket (peer uid > 0), the daemon auto-applies these RCTL + // rules to the operator's `crate-` loginclass after the + // jail is created. Idempotent — re-applying the same rule is + // a no-op at the kernel. + // + // Example crated.conf: + // rctl_umbrella: + // memoryuse: 4G + // pcpu: 200 + // maxproc: 256 + // + // Keys / values are validated via the existing RetunePure + // whitelist at config-load time; bad entries throw at startup. + std::vector> rctlUmbrella; + // Load from YAML file static Config load(const std::string &path); }; diff --git a/daemon/crated.conf.sample b/daemon/crated.conf.sample index f0bbd74..eae2466 100644 --- a/daemon/crated.conf.sample +++ b/daemon/crated.conf.sample @@ -214,3 +214,28 @@ log: # socket: /var/run/crate/crated-privops.sock # group: crate-operators # mode: "0660" + +## +## --- 0.9.29: per-loginclass RCTL umbrella defaults --- +## +## When a `create_jail` privops verb arrives over the libnv socket +## (peer uid > 0), crated auto-applies these RCTL rules to the +## operator's `crate-` loginclass after the jail is created. +## Idempotent — re-applying the same rule is a no-op at the kernel. +## +## With multiple jails per operator, the umbrella enforces an +## aggregate cap. If alice spawns 3 jails of 2G memoryuse each +## and the umbrella says memoryuse: 4G, the kernel rejects the +## third allocation (or fires the deny action) once total hits 4G. +## +## Keys / values validated via the same RetunePure whitelist as +## `crate retune --rctl`. Bad entries fail crated startup. +## +## Empty / unset block disables auto-application — the +## set_loginclass_rctl / clear_loginclass_rctl primitives from +## 0.9.28 still work on demand. +# +# rctl_umbrella: +# memoryuse: 4G +# pcpu: 200 +# maxproc: 256 diff --git a/daemon/main.cpp b/daemon/main.cpp index 1f969ed..1fa6ddf 100644 --- a/daemon/main.cpp +++ b/daemon/main.cpp @@ -8,6 +8,7 @@ #include "server.h" #include "ws_console.h" #include "control_socket.h" +#include "privops_handlers.h" #include "privops_listener.h" #include "err.h" @@ -113,6 +114,11 @@ int main(int argc, char **argv) { int csStarted = controlSockets.start(); // 0.9.14: privops listener (opt-in via privops_socket: in config). + // 0.9.29: register umbrella RCTL rules with the privops + // dispatcher; consulted post-create_jail when the operator's + // uid is known via getpeereid (libnv socket path). + Crated::setUmbrellaConfig(config.rctlUmbrella); + Crated::PrivopsListener privopsListener(config); bool privopsStarted = privopsListener.start(); diff --git a/daemon/privops_handlers.cpp b/daemon/privops_handlers.cpp index 0cd303b..caa7241 100644 --- a/daemon/privops_handlers.cpp +++ b/daemon/privops_handlers.cpp @@ -491,6 +491,42 @@ void maybeWritePerUserAudit(bool rootlessPerUser, uint32_t uid, appendPerUserAuditLine(uid, AuditPerUserPure::formatLine(r)); } +// 0.9.29: process-global RCTL umbrella config. Set once at daemon +// startup via setUmbrellaConfig(); read by maybeApplyUmbrella() +// after a successful create_jail privops invocation. Safe for +// concurrent reads after the one-time write at startup. +std::vector> g_umbrellaRules; + +// Apply umbrella rules to the operator's `crate-` loginclass +// after a successful create_jail. Only fires when: +// - operator's uid is known (uid > 0; libnv path supplies it, +// HTTP path always has uid=0 → no-op) +// - the verb was create_jail and returned 2xx +// - g_umbrellaRules is non-empty (operator opted in via +// `rctl_umbrella:` in crated.conf) +// +// Best-effort: rctl(8) failures are logged to stderr but do NOT +// fail the create_jail response — the jail is already up; losing +// an umbrella rule is a quota gap, not a correctness break. +void maybeApplyUmbrella(Verb v, uint32_t uid, int status) { + if (v != Verb::CreateJail) return; + if (uid == 0 || status < 200 || status >= 300) return; + if (g_umbrellaRules.empty()) return; + std::string loginclass = "crate-" + std::to_string(uid); + for (const auto &kv : g_umbrellaRules) { + std::string rule = "loginclass:" + loginclass + ":" + kv.first + + ":deny=" + kv.second; + try { + Util::execCommand({CRATE_PATH_RCTL, "-a", rule}, + "privops umbrella"); + } catch (const std::exception &e) { + std::fprintf(stderr, + "privops_handlers: umbrella rule '%s' for uid %u failed: %s\n", + rule.c_str(), (unsigned)uid, e.what()); + } + } +} + } // anon DispatchResult dispatchPrivOp(Verb v, const std::string &body, @@ -875,7 +911,14 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m, std::string("{\"error\":\"unknown or missing 'verb' field\"}")}; }(); maybeWritePerUserAudit(rootlessPerUser, operatorUid, v, result.status); + maybeApplyUmbrella(v, operatorUid, result.status); return result; } +// 0.9.29: setter for the process-global umbrella config. daemon/main.cpp +// calls this once at startup with the parsed `rctl_umbrella` block. +void setUmbrellaConfig(const std::vector> &rules) { + g_umbrellaRules = rules; +} + } // namespace Crated diff --git a/daemon/privops_handlers.h b/daemon/privops_handlers.h index 6dc100c..bf316ee 100644 --- a/daemon/privops_handlers.h +++ b/daemon/privops_handlers.h @@ -16,6 +16,8 @@ #include #include +#include +#include namespace Crated { @@ -175,4 +177,12 @@ PrivOpsWirePure::DispatchResult handleCreateEpair(const PrivOpsPure::CreateEpair PrivOpsWirePure::DispatchResult handleSetLoginclassRctl(const PrivOpsPure::SetLoginclassRctlReq &r); PrivOpsWirePure::DispatchResult handleClearLoginclassRctl(const PrivOpsPure::ClearLoginclassRctlReq &r); +// 0.9.29: register the daemon's `rctl_umbrella:` config. Called +// once at daemon startup. The umbrella rules apply after a +// successful create_jail privops invocation when the operator's +// uid is known (libnv socket; HTTP path doesn't have uid). Pass +// an empty vector to disable. Idempotent at the kernel level +// (rctl(8) treats re-set of an existing rule as a no-op). +void setUmbrellaConfig(const std::vector> &rules); + } // namespace Crated