@@ -20,6 +20,7 @@ import (
2020 "github.com/stacklok/apiary/internal/guest/mount"
2121 "github.com/stacklok/apiary/internal/guest/network"
2222 "github.com/stacklok/apiary/internal/guest/sshd"
23+ "github.com/stacklok/propolis/guest/harden"
2324)
2425
2526// Run executes the full guest boot sequence and returns a shutdown function
@@ -44,19 +45,43 @@ func Run(logger *slog.Logger) (shutdown func(), err error) {
4445 logger .Warn ("workspace mount failed, continuing without workspace" , "error" , err )
4546 }
4647
47- // 4. Load environment file.
48+ // 4. Apply kernel sysctl hardening (needs /proc mounted).
49+ harden .KernelDefaults (logger )
50+
51+ // 5. Lock down /root/ so the sandbox user cannot read it.
52+ lockdownRoot (logger )
53+
54+ // 6. Load environment file.
4855 envVars , err := env .Load ("/etc/sandbox-env" )
4956 if err != nil {
5057 return nil , fmt .Errorf ("loading environment: %w" , err )
5158 }
5259
53- // 5 . Parse authorized keys.
60+ // 7 . Parse authorized keys.
5461 authorizedKeys , err := parseAuthorizedKeys ("/home/sandbox/.ssh/authorized_keys" )
5562 if err != nil {
5663 return nil , fmt .Errorf ("parsing authorized keys: %w" , err )
5764 }
5865
59- // 6. Start SSH server — bind synchronously so listen errors surface
66+ // 8. Drop unneeded capabilities from the bounding set. Keep only
67+ // what sshd needs: SETUID/SETGID for credential switching,
68+ // NET_BIND_SERVICE for port 22. This must be the last privileged
69+ // operation before starting the SSH server.
70+ //
71+ // This is fatal because PR_CAPBSET_DROP has been available since
72+ // Linux 2.6.25 — failure indicates a serious problem (not root,
73+ // kernel bug) and continuing with a full bounding set defeats the
74+ // hardening entirely.
75+ logger .Info ("dropping unnecessary capabilities" )
76+ if err := harden .DropBoundingCaps (
77+ harden .CapSetUID ,
78+ harden .CapSetGID ,
79+ harden .CapNetBindService ,
80+ ); err != nil {
81+ return nil , fmt .Errorf ("dropping capabilities: %w" , err )
82+ }
83+
84+ // 9. Start SSH server — bind synchronously so listen errors surface
6085 // immediately rather than being swallowed in a goroutine.
6186 cfg := sshd.Config {
6287 Port : 22 ,
@@ -90,6 +115,15 @@ func Run(logger *slog.Logger) (shutdown func(), err error) {
90115 return func () { srv .Close () }, nil
91116}
92117
118+ // lockdownRoot sets /root/ to mode 0700 so the sandbox user cannot read
119+ // its contents (MCP bootstrap config, debug logs, etc.).
120+ func lockdownRoot (logger * slog.Logger ) {
121+ logger .Info ("locking down /root permissions" )
122+ if err := os .Chmod ("/root" , 0o700 ); err != nil {
123+ logger .Warn ("failed to chmod /root" , "error" , err )
124+ }
125+ }
126+
93127// parseAuthorizedKeys reads an authorized_keys file and returns the parsed
94128// public keys. Returns an error if no valid keys are found.
95129func parseAuthorizedKeys (path string ) ([]ssh.PublicKey , error ) {
0 commit comments