diff --git a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java index c397ea9..6106af5 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java +++ b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java @@ -121,6 +121,7 @@ public final class Fleetd { // else can fail on a silently-empty one. A daemon started without a login shell (launchd) // boots fine either way — this is the only thing that says so out loud. reportRequiredSecrets(cfg); + reportMemberTrustModel(cfg); // CB-596: an absent (or empty) memberCredentials: block blocks NOTHING — no credential // name is hardcoded any more to fall back on. Say so loudly, the same way a missing // secret is reported above, so upgrading past this commit never silently drops CB-592's @@ -1136,6 +1137,24 @@ public final class Fleetd { }); } + /** + * fleetd #184: state the member trust model at startup. Environment controls and worktrees do + * not make a sandbox when fleetd and its members use the same OS user. A separate herdr may + * provide that boundary, but fleetd cannot inspect the uid at the other end of its socket. + */ + static void reportMemberTrustModel(FleetConfig cfg) { + if (cfg.memberHerdrSocket() != null && !cfg.memberHerdrSocket().isBlank()) { + log.info("member trust model: members are routed to a separate herdr through " + + "memberHerdrSocket. fleetd cannot see that herdr's uid, so confirm it runs " + + "as a different OS user before treating it as a boundary."); + return; + } + log.info("member trust model: members run as the same OS user as fleetd, not in a sandbox. " + + "A member can read any file this user can read, including SSH keys and credential " + + "stores, whatever memberCredentials says. To add a real boundary, route members to " + + "a second herdr under a different OS user with memberHerdrSocket."); + } + /** * CB-596: {@code known:} empty (block absent entirely, or present but empty) means {@link * FleetConfig.MemberCredentials#blockedSet()} is empty too — every member pane inherits the diff --git a/fleetd/src/test/java/dev/ltms/fleet/MemberTrustModelReportTest.java b/fleetd/src/test/java/dev/ltms/fleet/MemberTrustModelReportTest.java new file mode 100644 index 0000000..63fa725 --- /dev/null +++ b/fleetd/src/test/java/dev/ltms/fleet/MemberTrustModelReportTest.java @@ -0,0 +1,73 @@ +package dev.ltms.fleet; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import dev.ltms.fleet.config.FleetConfig; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * fleetd #184: startup must state whether members share fleetd's OS user or use a separate herdr. + */ +class MemberTrustModelReportTest { + + private static FleetConfig load(Path dir, String yaml) throws Exception { + Path f = dir.resolve("fleetd.yaml"); + Files.writeString(f, yaml); + return FleetConfig.load(f); + } + + private static ListAppender attach() { + Logger logger = (Logger) LoggerFactory.getLogger(Fleetd.class); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + return appender; + } + + private static void detach(ListAppender appender) { + ((Logger) LoggerFactory.getLogger(Fleetd.class)).detachAppender(appender); + } + + private static String report(FleetConfig cfg) { + Logger logger = (Logger) LoggerFactory.getLogger(Fleetd.class); + ch.qos.logback.classic.Level original = logger.getLevel(); + logger.setLevel(ch.qos.logback.classic.Level.INFO); + ListAppender appender = attach(); + try { + Fleetd.reportMemberTrustModel(cfg); + } finally { + detach(appender); + logger.setLevel(original); + } + return appender.list.getFirst().getFormattedMessage(); + } + + @Test + void unsetMemberHerdrSocketStatesThatMembersAreNotSandboxed(@TempDir Path dir) throws Exception { + FleetConfig cfg = load(dir, "bind:\n host: 127.0.0.1\n port: 8765\n"); + + assertEquals("member trust model: members run as the same OS user as fleetd, not in a sandbox. " + + "A member can read any file this user can read, including SSH keys and credential " + + "stores, whatever memberCredentials says. To add a real boundary, route members to " + + "a second herdr under a different OS user with memberHerdrSocket.", + report(cfg)); + } + + @Test + void configuredMemberHerdrSocketStatesThatFleetdCannotConfirmTheBoundary(@TempDir Path dir) throws Exception { + FleetConfig cfg = load(dir, "memberHerdrSocket: /tmp/member-herdr.sock\n"); + + assertEquals("member trust model: members are routed to a separate herdr through " + + "memberHerdrSocket. fleetd cannot see that herdr's uid, so confirm it runs " + + "as a different OS user before treating it as a boundary.", + report(cfg)); + } +}