Skip to content

Commit f858795

Browse files
jrwrenClaude
andauthored
add sshd.sandbox_dir config option (#1622)
* add sshd.sandbox_dir config option Sanitize SSH profile paths (ssh.go:514,683,719) — restrict os.Create(a[0]) to a safe directory. Add a config option in the config file to specify the sandbox directory. For backwards compatibility, if the config is not specified, keep the current behavior. * update default and example * use os.TempDir() for sshd.sandbox_dir default * split sandbox path validation into separate conditionals Separate the combined && check in sshSanitizeFilePath into two distinct conditionals with specific error messages: one for paths resolving to the sandbox directory itself, and one for paths outside the sandbox. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com> * fix: trim leading zeros from p256 signature swap result bigmod.Nat.Bytes() returns fixed-size 32-byte slices, but ASN.1 INTEGER parsing strips leading zeros. This caused a flaky test failure (~1/256 chance) when the S value's high byte was zero. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com> --------- Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
1 parent 951d368 commit f858795

3 files changed

Lines changed: 74 additions & 10 deletions

File tree

cert/p256/p256.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ func swap(r, s []byte) ([]byte, []byte, error) {
4444
}
4545
sNormalized := nMod.Nat().Sub(bigS, nMod)
4646

47-
return r, sNormalized.Bytes(nMod), nil
47+
result := sNormalized.Bytes(nMod)
48+
for len(result) > 1 && result[0] == 0 {
49+
result = result[1:]
50+
}
51+
52+
return r, result, nil
4853
}
4954

5055
func Normalize(sig []byte) ([]byte, error) {

examples/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ punchy:
204204
# Trusted SSH CA public keys. These are the public keys of the CAs that are allowed to sign SSH keys for access.
205205
#trusted_cas:
206206
#- "ssh public key string"
207+
# sandbox_dir restricts file paths for profiling commands (start-cpu-profile, save-heap-profile,
208+
# save-mutex-profile) to the specified directory. Relative paths will be resolved within this directory,
209+
# and absolute paths outside of it will be rejected. Default is $TMP/nebula-debug.
210+
# The directory is NOT automatically created.
211+
# Overriding this to "" is the same as "/" and will allow overwriting any path on the host.
212+
#sandbox_dir: /var/tmp/nebula-debug
207213

208214
# EXPERIMENTAL: relay support for networks that can't establish direct connections.
209215
relay:

ssh.go

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net"
1111
"net/netip"
1212
"os"
13+
"path/filepath"
1314
"reflect"
1415
"runtime"
1516
"runtime/pprof"
@@ -188,6 +189,12 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
188189
}
189190

190191
func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Interface) {
192+
// sandboxDir defaults to a dir in temp. The intention is that end user will
193+
// create this dir as needed. Overriding this config value to "" allows
194+
// writing to anywhere in the system.
195+
defaultDir := filepath.Join(os.TempDir(), "nebula-debug")
196+
sandboxDir := c.GetString("sshd.sandbox_dir", defaultDir)
197+
191198
ssh.RegisterCommand(&sshd.Command{
192199
Name: "list-hostmap",
193200
ShortDescription: "List all known previously connected hosts",
@@ -246,7 +253,9 @@ func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Inter
246253
ssh.RegisterCommand(&sshd.Command{
247254
Name: "start-cpu-profile",
248255
ShortDescription: "Starts a cpu profile and write output to the provided file, ex: `cpu-profile.pb.gz`",
249-
Callback: sshStartCpuProfile,
256+
Callback: func(fs any, a []string, w sshd.StringWriter) error {
257+
return sshStartCpuProfile(sandboxDir, fs, a, w)
258+
},
250259
})
251260

252261
ssh.RegisterCommand(&sshd.Command{
@@ -261,7 +270,9 @@ func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Inter
261270
ssh.RegisterCommand(&sshd.Command{
262271
Name: "save-heap-profile",
263272
ShortDescription: "Saves a heap profile to the provided path, ex: `heap-profile.pb.gz`",
264-
Callback: sshGetHeapProfile,
273+
Callback: func(fs any, a []string, w sshd.StringWriter) error {
274+
return sshGetHeapProfile(sandboxDir, fs, a, w)
275+
},
265276
})
266277

267278
ssh.RegisterCommand(&sshd.Command{
@@ -273,7 +284,9 @@ func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Inter
273284
ssh.RegisterCommand(&sshd.Command{
274285
Name: "save-mutex-profile",
275286
ShortDescription: "Saves a mutex profile to the provided path, ex: `mutex-profile.pb.gz`",
276-
Callback: sshGetMutexProfile,
287+
Callback: func(fs any, a []string, w sshd.StringWriter) error {
288+
return sshGetMutexProfile(sandboxDir, fs, a, w)
289+
},
277290
})
278291

279292
ssh.RegisterCommand(&sshd.Command{
@@ -506,13 +519,43 @@ func sshListLighthouseMap(lightHouse *LightHouse, a any, w sshd.StringWriter) er
506519
return nil
507520
}
508521

509-
func sshStartCpuProfile(fs any, a []string, w sshd.StringWriter) error {
522+
// sshSanitizeFilePath validates that the given file path is within the sandbox directory.
523+
// If sandboxDir is empty, the path is returned as-is for backwards compatibility.
524+
func sshSanitizeFilePath(sandboxDir, filePath string) (string, error) {
525+
if sandboxDir == "" {
526+
return filePath, nil
527+
}
528+
529+
// Clean and resolve the path relative to the sandbox directory
530+
if !filepath.IsAbs(filePath) {
531+
filePath = filepath.Join(sandboxDir, filePath)
532+
}
533+
cleaned := filepath.Clean(filePath)
534+
535+
// Ensure the resolved path is within the sandbox directory
536+
cleanedSandbox := filepath.Clean(sandboxDir)
537+
if cleaned == cleanedSandbox {
538+
return "", fmt.Errorf("path %q resolves to the sandbox directory itself %q", filePath, sandboxDir)
539+
}
540+
if !strings.HasPrefix(cleaned, cleanedSandbox+string(filepath.Separator)) {
541+
return "", fmt.Errorf("path %q is outside the sandbox directory %q", filePath, sandboxDir)
542+
}
543+
544+
return cleaned, nil
545+
}
546+
547+
func sshStartCpuProfile(sandboxDir string, fs any, a []string, w sshd.StringWriter) error {
510548
if len(a) == 0 {
511549
err := w.WriteLine("No path to write profile provided")
512550
return err
513551
}
514552

515-
file, err := os.Create(a[0])
553+
filePath, err := sshSanitizeFilePath(sandboxDir, a[0])
554+
if err != nil {
555+
return w.WriteLine(err.Error())
556+
}
557+
558+
file, err := os.Create(filePath)
516559
if err != nil {
517560
err = w.WriteLine(fmt.Sprintf("Unable to create profile file: %s", err))
518561
return err
@@ -676,12 +719,17 @@ func sshChangeRemote(ifce *Interface, fs any, a []string, w sshd.StringWriter) e
676719
return w.WriteLine("Changed")
677720
}
678721

679-
func sshGetHeapProfile(fs any, a []string, w sshd.StringWriter) error {
722+
func sshGetHeapProfile(sandboxDir string, fs any, a []string, w sshd.StringWriter) error {
680723
if len(a) == 0 {
681724
return w.WriteLine("No path to write profile provided")
682725
}
683726

684-
file, err := os.Create(a[0])
727+
filePath, err := sshSanitizeFilePath(sandboxDir, a[0])
728+
if err != nil {
729+
return w.WriteLine(err.Error())
730+
}
731+
732+
file, err := os.Create(filePath)
685733
if err != nil {
686734
err = w.WriteLine(fmt.Sprintf("Unable to create profile file: %s", err))
687735
return err
@@ -712,12 +760,17 @@ func sshMutexProfileFraction(fs any, a []string, w sshd.StringWriter) error {
712760
return w.WriteLine(fmt.Sprintf("New value: %d. Old value: %d", newRate, oldRate))
713761
}
714762

715-
func sshGetMutexProfile(fs any, a []string, w sshd.StringWriter) error {
763+
func sshGetMutexProfile(sandboxDir string, fs any, a []string, w sshd.StringWriter) error {
716764
if len(a) == 0 {
717765
return w.WriteLine("No path to write profile provided")
718766
}
719767

720-
file, err := os.Create(a[0])
768+
filePath, err := sshSanitizeFilePath(sandboxDir, a[0])
769+
if err != nil {
770+
return w.WriteLine(err.Error())
771+
}
772+
773+
file, err := os.Create(filePath)
721774
if err != nil {
722775
return w.WriteLine(fmt.Sprintf("Unable to create profile file: %s", err))
723776
}

0 commit comments

Comments
 (0)