From 1837dd1178a12ebd03f0fa1b6fe66a4354f007f7 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Wed, 29 Apr 2026 10:25:06 +0300 Subject: [PATCH] knotserver: add secure-mode config and server wiring --- knotserver/config/config.go | 3 ++ knotserver/server.go | 63 ++++++++++++++++++++++++++++------ knotserver/xrpc/create_repo.go | 34 +++++++++++++++++- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/knotserver/config/config.go b/knotserver/config/config.go index e6bcd92b..91dd5d92 100644 --- a/knotserver/config/config.go +++ b/knotserver/config/config.go @@ -27,6 +27,9 @@ type Server struct { // This disables signature verification so use with caution. Dev bool `env:"DEV, default=false"` + + // SecureMode enables per-repository subprocess isolation. + SecureMode bool `env:"SECURE_MODE, default=false"` } type Git struct { diff --git a/knotserver/server.go b/knotserver/server.go index df7f397d..099eee0c 100644 --- a/knotserver/server.go +++ b/knotserver/server.go @@ -12,7 +12,8 @@ import ( "tangled.org/core/idresolver" "tangled.org/core/jetstream" "tangled.org/core/knotserver/config" - "tangled.org/core/knotserver/db" + knotdb "tangled.org/core/knotserver/db" + "tangled.org/core/knotserver/sandbox" "tangled.org/core/log" "tangled.org/core/notifier" "tangled.org/core/rbac" @@ -23,6 +24,12 @@ func Command() *cli.Command { Name: "server", Usage: "run a knot server", Action: Run, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "secure-mode", + Usage: "isolate git subprocesses to their own repository directory", + }, + }, Description: ` Environment variables: KNOT_SERVER_LISTEN_ADDR (default: 0.0.0.0:5555) @@ -33,6 +40,7 @@ func Command() *cli.Command { KNOT_SERVER_OWNER (required) KNOT_SERVER_LOG_DIDS (default: true) KNOT_SERVER_DEV (default: false) + KNOT_SERVER_SECURE_MODE (default: false) KNOT_REPO_SCAN_PATH (default: /home/git) KNOT_REPO_README (comma-separated list) KNOT_REPO_MAIN_BRANCH (default: main) @@ -53,20 +61,27 @@ func Run(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to load config: %w", err) } - err = hook.Setup(hook.Config( - hook.WithScanPath(c.Repo.ScanPath), - hook.WithInternalApi(c.Server.InternalListenAddr), - )) - if err != nil { - return fmt.Errorf("failed to setup hooks: %w", err) + // CLI flag overrides env var. + if cmd.Bool("secure-mode") { + c.Server.SecureMode = true + } + + if !c.Server.SecureMode { + err = hook.Setup(hook.Config( + hook.WithScanPath(c.Repo.ScanPath), + hook.WithInternalApi(c.Server.InternalListenAddr), + )) + if err != nil { + return fmt.Errorf("failed to setup hooks: %w", err) + } + logger.Info("successfully finished setting up hooks") } - logger.Info("successfully finished setting up hooks") if c.Server.Dev { logger.Info("running in dev mode, signature verification is disabled") } - db, err := db.Setup(ctx, c.Server.DBPath) + db, err := knotdb.Setup(ctx, c.Server.DBPath) if err != nil { return fmt.Errorf("failed to load db: %w", err) } @@ -97,9 +112,37 @@ func Run(ctx context.Context, cmd *cli.Command) error { logger.Warn("knot members backfill failed, continuing", "err", err) } + // probe and initialise the sandbox backend. + var sb sandbox.Backend + if c.Server.SecureMode { + var warn string + sb, warn = sandbox.New(func(repoPath string) (uint32, uint32, error) { + return sandbox.LookupUIDForRepoPath(c.Repo.ScanPath, repoPath) + }) + if warn != "" { + return fmt.Errorf("secure-mode: %s", warn) + } + logger.Info("secure-mode: activated", "backend", sb.Name()) + + // refuse to start if any repos have not yet been isolation-migrated. + unmigrated, countErr := db.CountUnmigratedRepos() + if countErr != nil { + return fmt.Errorf("secure-mode: checking unmigrated repos: %w", countErr) + } + if unmigrated > 0 { + return fmt.Errorf( + "secure-mode: %d repo(s) have not been isolation-migrated; "+ + "run 'knot migrate-isolation' first", + unmigrated, + ) + } + } else { + sb = &sandbox.NoopBackend{} + } + go migrateReposOnStartup(ctx, c, db, e, ¬ifier, log.SubLogger(logger, "migrate")) - mux, err := Setup(ctx, c, db, e, jc, ¬ifier, resolver) + mux, err := Setup(ctx, c, db, e, jc, ¬ifier, resolver, sb) if err != nil { return fmt.Errorf("failed to setup server: %w", err) } diff --git a/knotserver/xrpc/create_repo.go b/knotserver/xrpc/create_repo.go index c4b73441..6db0271b 100644 --- a/knotserver/xrpc/create_repo.go +++ b/knotserver/xrpc/create_repo.go @@ -204,7 +204,7 @@ func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) { } if data.Source != nil && *data.Source != "" { - err = git.Fork(repoPath, *data.Source, h.Config) + err = git.ForkWithSandbox(repoPath, *data.Source, h.Config, h.Sandbox) if err != nil { l.Error("forking repo", "error", err.Error()) cleanupAll() @@ -261,6 +261,38 @@ func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) { return } + if h.Config.Server.SecureMode { + ownerUID, err := h.Db.GetOrAssignOwnerUID(actorDid.String()) + if err != nil { + l.Error("failed to get/assign owner uid", "error", err.Error()) + cleanupAll() + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + if err := sandbox.ChmodRepoTree(repoPath); err != nil { + l.Error("failed to chmod repo tree", "error", err.Error()) + cleanupAll() + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + serviceGid, err := sandbox.ServiceGid(h.Config.Repo.ScanPath) + if err != nil { + l.Error("failed to resolve service gid", "error", err.Error()) + cleanupAll() + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + if err := sandbox.ChownRepoTree(repoPath, int(ownerUID), int(serviceGid)); err != nil { + l.Error("failed to chown repo tree", "error", err.Error()) + cleanupAll() + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + if err := h.Db.MarkRepoIsolated(repoDid); err != nil { + l.Error("failed to mark repo isolated", "error", err.Error()) + } + } + if prepared != nil { plcCtx, plcCancel := context.WithTimeout(context.Background(), 30*time.Second) defer plcCancel() -- 2.51.2