carlos / platform Public

bootstrap: create and arm the isolated-exec trees, mounts and control directory (plan 1, task 5)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ1ZvWj7naf73GjTX8MBv5
Paul Campbell pushed by paul@keymail.dev a0c05203abc7976bae5c5390538173bffa9629c4 parent ce07753
3 files changed, +131 −4
  • internal/host/bootstrap.go +48 −3
  • internal/host/host_test.go +2 −1
  • internal/host/units_isolation_test.go +81 −0

CI passed — run details

diff --git a/internal/host/bootstrap.go b/internal/host/bootstrap.go
index bc9175c0d..b4ba32877 100644
--- a/internal/host/bootstrap.go
+++ b/internal/host/bootstrap.go
@@ -59,7 +59,29 @@ const socketsGroup = "carlos-sockets"
// carlos:carlos, so consumer enrollment deliberately does not reach unit
// tenants. other=0 on both dirs keeps every unenrolled uid out.
const tmpfilesConf = "d /run/carlos 2750 " + serviceUser + " " + socketsGroup + " -\n" +
- "d /run/carlos/apps 0750 " + serviceUser + " " + serviceUser + " -\n"
+ "d /run/carlos/apps 0750 " + serviceUser + " " + serviceUser + " -\n" +
+ // The isolated-exec trees (design §3.1, §3.2). Each protected view is
+ // a nosymfollow bind of the dotted raw directory beside it (the mount
+ // units); tmpfiles creates both every boot because /run is tmpfs and
+ // because the mounts are ordered after it. The socket trees keep the
+ // consumers group and setgid, exactly like /run/carlos above, so an
+ // enrolled router can still dial a migrated instance (issue #103).
+ "d /data/.instances 0700 " + serviceUser + " " + serviceUser + " -\n" +
+ "d /data/instances 0755 " + serviceUser + " " + serviceUser + " -\n" +
+ "d /data/layout 0700 " + serviceUser + " " + serviceUser + " -\n" +
+ "d /run/carlos/.instances 2750 " + serviceUser + " " + socketsGroup + " -\n" +
+ "d /run/carlos/instances 2750 " + serviceUser + " " + socketsGroup + " -\n" +
+ "d /run/carlos/.logs 0700 " + serviceUser + " " + serviceUser + " -\n" +
+ "d /run/carlos/logs 0700 " + serviceUser + " " + serviceUser + " -\n" +
+ // The root instance-control channel's directory (design §5.2): root's,
+ // so no carlos process can create, rename or replace anything in it;
+ // group carlos with search so the agent can connect.
+ "d /run/carlos-control 0750 root " + serviceUser + " -\n" +
+ // The readiness probe's fixture (design §8.5, codex R11-1): a file
+ // root:carlos 0640 that an isolated instance must NOT be able to read.
+ // Written here, not by the control service, whose ProtectSystem=strict
+ // makes /etc read-only to it.
+ "f /etc/carlos/probe-secret 0640 root " + serviceUser + " - probe\n"
// journaldConfName is the journald drop-in every box gets (discussion #111).
//
@@ -114,7 +136,9 @@ const journaldConf = "# Written by carlos bootstrap (discussion #111). Do not ed
const relayUnitName = "carlos-relay.service"
var enabledUnits = []string{"carlos-edge.service", relayUnitName, "carlos-adopt.timer", "carlos-adopt.path", "carlos-stage.path", "carlos-restart.path",
- "carlos-system-update.path", "carlos-system-recover.service"}
+ "carlos-system-update.path", "carlos-system-recover.service",
+ // The isolated-exec mounts (design §3.2): the edge Requires= them.
+ "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount"}
// inertUnits are installed everywhere and deliberately never armed by
// bootstrap or by a self-update apply: restore-verify until an operator
@@ -162,6 +186,12 @@ func currentBootstrapPlan() bootstrapPlan {
// a failed condition, not a failure — which is what makes starting it safe
// fleet-wide. On a box that IS armed, starting it here is what saves a
// reboot.
+//
+// Socket and mount units are armed the same way (isolated-exec design
+// §8.6, codex R1-14): an enabled-but-unstarted socket is a control channel
+// nobody is listening on, and an enabled-but-unmounted bind is an instance
+// tree with no protection, until the next reboot. Starting either is
+// idempotent.
func systemctlPlan(units []string) [][]string {
plan := [][]string{
{"daemon-reload"},
@@ -169,7 +199,8 @@ func systemctlPlan(units []string) [][]string {
}
var armed []string
for _, u := range units {
- if strings.HasSuffix(u, ".timer") || strings.HasSuffix(u, ".path") || u == relayUnitName {
+ if strings.HasSuffix(u, ".timer") || strings.HasSuffix(u, ".path") ||
+ strings.HasSuffix(u, ".socket") || strings.HasSuffix(u, ".mount") || u == relayUnitName {
armed = append(armed, u)
}
}
@@ -306,6 +337,20 @@ func bootstrapWith(root string, consumers []string, planJSON bool) error {
// The carlos-app@ tenant sockets (see tmpfilesConf for the perms
// rationale).
{filepath.Join(runDir, "apps"), 0o750},
+ // The isolated-exec trees (design §3.1). The dotted names are the
+ // raw directories the mount units bind onto the undotted views;
+ // see tmpfilesConf for the modes and why both exist.
+ {filepath.Join(dataDir, ".instances"), 0o700},
+ {filepath.Join(dataDir, "instances"), 0o755},
+ {filepath.Join(dataDir, "layout"), 0o700},
+ {filepath.Join(runDir, ".instances"), 0o750 | os.ModeSetgid},
+ {filepath.Join(runDir, "instances"), 0o750 | os.ModeSetgid},
+ {filepath.Join(runDir, ".logs"), 0o700},
+ {filepath.Join(runDir, "logs"), 0o700},
+ // Root-owned on a live box (privilegedPlan chowns /run/carlos
+ // recursively to the service user; this one sits BESIDE it, not
+ // under it, precisely so that chown never reaches it).
+ {filepath.Join(root, "run", "carlos-control"), 0o750},
// The version store + per-host live symlinks (release/versions.go).
// World-readable/executable so sandboxed instance units can exec their
// binary; only root (the adopt pass) writes here.
diff --git a/internal/host/host_test.go b/internal/host/host_test.go
index 9dbc0a1d7..37558c533 100644
--- a/internal/host/host_test.go
+++ b/internal/host/host_test.go
@@ -1216,7 +1216,8 @@ func TestProbedUnitsAreTheOnesBootstrapArms(t *testing.T) {
func TestBootstrapPlanJSONNamesWhatALiveRunArms(t *testing.T) {
p := currentBootstrapPlan()
want := map[string]bool{"carlos-edge.service": true, "carlos-adopt.timer": true, "carlos-adopt.path": true,
- "carlos-stage.path": true, "carlos-restart.path": true, "carlos-system-update.path": true, "carlos-system-recover.service": true}
+ "carlos-stage.path": true, "carlos-restart.path": true, "carlos-system-update.path": true, "carlos-system-recover.service": true,
+ "data-instances.mount": true, "run-carlos-instances.mount": true, "run-carlos-logs.mount": true}
got := map[string]bool{}
for _, u := range p.Enabled {
got[u] = true
diff --git a/internal/host/units_isolation_test.go b/internal/host/units_isolation_test.go
index 27a770336..db678d790 100644
--- a/internal/host/units_isolation_test.go
+++ b/internal/host/units_isolation_test.go
@@ -1,6 +1,8 @@
package host
import (
+ "os"
+ "path/filepath"
"strings"
"testing"
)
@@ -116,3 +118,82 @@ func TestEmbeddedUnitsIncludeSlicesAndMounts(t *testing.T) {
}
}
}
+
+// tmpfiles creates the raw directories and the mountpoints every boot
+// (/run is tmpfs), with the modes the design gives them (§3.1, §3.2):
+// the socket trees keep the carlos-sockets group and setgid (issue #103),
+// the log tree and data trees are the agent's alone, and the control
+// directory is root's so no carlos process can create anything in it.
+func TestTmpfilesCreatesTheInstanceTrees(t *testing.T) {
+ for _, want := range []string{
+ "d /data/.instances 0700 carlos carlos -",
+ "d /data/instances 0755 carlos carlos -",
+ "d /data/layout 0700 carlos carlos -",
+ "d /run/carlos/.instances 2750 carlos carlos-sockets -",
+ "d /run/carlos/instances 2750 carlos carlos-sockets -",
+ "d /run/carlos/.logs 0700 carlos carlos -",
+ "d /run/carlos/logs 0700 carlos carlos -",
+ "d /run/carlos-control 0750 root carlos -",
+ "f /etc/carlos/probe-secret 0640 root carlos - probe",
+ } {
+ if !strings.Contains(tmpfilesConf, want+"\n") {
+ t.Errorf("tmpfilesConf lacks %q", want)
+ }
+ }
+ // The existing lines stay: the flat socket dir and the apps subtree.
+ for _, keep := range []string{"d /run/carlos 2750 carlos carlos-sockets -", "d /run/carlos/apps 0750 carlos carlos -"} {
+ if !strings.Contains(tmpfilesConf, keep+"\n") {
+ t.Errorf("tmpfilesConf lost %q", keep)
+ }
+ }
+}
+
+// bootstrap -root <dir> creates the same directories in the staging tree
+// (a live run's chown -R covers them), so a fresh box has them before the
+// mounts try to bind them.
+func TestBootstrapCreatesTheInstanceDirectories(t *testing.T) {
+ root := t.TempDir()
+ if err := bootstrap(root, nil); err != nil {
+ t.Fatalf("bootstrap: %v", err)
+ }
+ for _, d := range []string{"data/.instances", "data/instances", "data/layout", "run/carlos/.instances", "run/carlos/instances", "run/carlos/.logs", "run/carlos/logs", "run/carlos-control"} {
+ if fi, err := os.Stat(filepath.Join(root, d)); err != nil || !fi.IsDir() {
+ t.Errorf("%s: %v", d, err)
+ }
+ }
+}
+
+// The mounts are enabled AND started by the plan: `systemctl enable` on a
+// mount unit writes a symlink for the next boot and nothing else, the same
+// trap timers and path units fell into (codex R1-14). A socket unit the
+// same. The relay is still the only service the plan starts.
+func TestSystemctlPlanStartsMountsAndSockets(t *testing.T) {
+ plan := systemctlPlan([]string{"carlos-edge.service", "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount", "carlos-adopt.timer", "carlos-instance-control.socket"})
+ var started []string
+ for _, args := range plan {
+ if args[0] == "start" {
+ started = append(started, args[1:]...)
+ }
+ }
+ got := strings.Join(started, " ")
+ for _, want := range []string{"data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount", "carlos-adopt.timer", "carlos-instance-control.socket"} {
+ if !strings.Contains(got, want) {
+ t.Errorf("plan does not start %s: %v", want, plan)
+ }
+ }
+ if strings.Contains(got, "carlos-edge.service") {
+ t.Errorf("plan starts the edge: %v", plan)
+ }
+}
+
+func TestEnabledUnitsIncludeTheMounts(t *testing.T) {
+ have := map[string]bool{}
+ for _, u := range enabledUnits {
+ have[u] = true
+ }
+ for _, want := range []string{"data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount"} {
+ if !have[want] {
+ t.Errorf("enabledUnits lacks %s", want)
+ }
+ }
+}