From 2fd4c45b34e1b4b9fa55ca0a70a3be7bb1132a1d Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Tue, 14 Jan 2025 08:59:11 +0000 Subject: [PATCH] smb: add support for kerberos authentication Fixes #7800 --- backend/smb/connpool.go | 22 ++++++- backend/smb/kerberos.go | 78 ++++++++++++++++++++++++ backend/smb/smb.go | 11 ++++ backend/smb/smb_test.go | 11 ++++ fstest/test_all/config.yaml | 3 + fstest/testserver/init.d/PORTS.md | 2 + fstest/testserver/init.d/TestSMBKerberos | 74 ++++++++++++++++++++++ 7 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 backend/smb/kerberos.go create mode 100755 fstest/testserver/init.d/TestSMBKerberos diff --git a/backend/smb/connpool.go b/backend/smb/connpool.go index 13a8c3042..2c3fb2f80 100644 --- a/backend/smb/connpool.go +++ b/backend/smb/connpool.go @@ -31,13 +31,29 @@ func (f *Fs) dial(ctx context.Context, network, addr string) (*conn, error) { } } - d := &smb2.Dialer{ - Initiator: &smb2.NTLMInitiator{ + d := &smb2.Dialer{} + if f.opt.UseKerberos { + cl, err := getKerberosClient() + if err != nil { + return nil, err + } + + spn := f.opt.SPN + if spn == "" { + spn = "cifs/" + f.opt.Host + } + + d.Initiator = &smb2.Krb5Initiator{ + Client: cl, + TargetSPN: spn, + } + } else { + d.Initiator = &smb2.NTLMInitiator{ User: f.opt.User, Password: pass, Domain: f.opt.Domain, TargetSPN: f.opt.SPN, - }, + } } session, err := d.DialConn(ctx, tconn, addr) diff --git a/backend/smb/kerberos.go b/backend/smb/kerberos.go new file mode 100644 index 000000000..a00608de7 --- /dev/null +++ b/backend/smb/kerberos.go @@ -0,0 +1,78 @@ +package smb + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + "sync" + + "github.com/jcmturner/gokrb5/v8/client" + "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/credentials" +) + +var ( + kerberosClient *client.Client + kerberosErr error + kerberosOnce sync.Once +) + +// getKerberosClient returns a Kerberos client that can be used to authenticate. +func getKerberosClient() (*client.Client, error) { + if kerberosClient == nil || kerberosErr == nil { + kerberosOnce.Do(func() { + kerberosClient, kerberosErr = createKerberosClient() + }) + } + + return kerberosClient, kerberosErr +} + +// createKerberosClient creates a new Kerberos client. +func createKerberosClient() (*client.Client, error) { + cfgPath := os.Getenv("KRB5_CONFIG") + if cfgPath == "" { + cfgPath = "/etc/krb5.conf" + } + + cfg, err := config.Load(cfgPath) + if err != nil { + return nil, err + } + + // Determine the ccache location from the environment, falling back to the + // default location. + ccachePath := os.Getenv("KRB5CCNAME") + switch { + case strings.Contains(ccachePath, ":"): + parts := strings.SplitN(ccachePath, ":", 2) + switch parts[0] { + case "FILE": + ccachePath = parts[1] + case "DIR": + primary, err := os.ReadFile(filepath.Join(parts[1], "primary")) + if err != nil { + return nil, err + } + ccachePath = filepath.Join(parts[1], strings.TrimSpace(string(primary))) + default: + return nil, fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath) + } + case ccachePath == "": + u, err := user.Current() + if err != nil { + return nil, err + } + + ccachePath = "/tmp/krb5cc_" + u.Uid + } + + ccache, err := credentials.LoadCCache(ccachePath) + if err != nil { + return nil, err + } + + return client.NewFromCCache(ccache, cfg) +} diff --git a/backend/smb/smb.go b/backend/smb/smb.go index ff8f40976..0fb1df8b9 100644 --- a/backend/smb/smb.go +++ b/backend/smb/smb.go @@ -76,6 +76,16 @@ authentication, and it often needs to be set for clusters. For example: Leave blank if not sure. `, Sensitive: true, + }, { + Name: "use_kerberos", + Help: `Use Kerberos authentication. + +If set, rclone will use Kerberos authentication instead of NTLM. This +requires a valid Kerberos configuration and credentials cache to be +available, either in the default locations or as specified by the +KRB5_CONFIG and KRB5CCNAME environment variables. +`, + Default: false, }, { Name: "idle_timeout", Default: fs.Duration(60 * time.Second), @@ -126,6 +136,7 @@ type Options struct { Pass string `config:"pass"` Domain string `config:"domain"` SPN string `config:"spn"` + UseKerberos bool `config:"use_kerberos"` HideSpecial bool `config:"hide_special_share"` CaseInsensitive bool `config:"case_insensitive"` IdleTimeout fs.Duration `config:"idle_timeout"` diff --git a/backend/smb/smb_test.go b/backend/smb/smb_test.go index aa5a0e419..f22bdb7f9 100644 --- a/backend/smb/smb_test.go +++ b/backend/smb/smb_test.go @@ -2,6 +2,7 @@ package smb_test import ( + "path/filepath" "testing" "github.com/rclone/rclone/backend/smb" @@ -15,3 +16,13 @@ func TestIntegration(t *testing.T) { NilObject: (*smb.Object)(nil), }) } + +func TestIntegration2(t *testing.T) { + krb5Dir := t.TempDir() + t.Setenv("KRB5_CONFIG", filepath.Join(krb5Dir, "krb5.conf")) + t.Setenv("KRB5CCNAME", filepath.Join(krb5Dir, "ccache")) + fstests.Run(t, &fstests.Opt{ + RemoteName: "TestSMBKerberos:rclone", + NilObject: (*smb.Object)(nil), + }) +} diff --git a/fstest/test_all/config.yaml b/fstest/test_all/config.yaml index 624d875d5..6c5c85c31 100644 --- a/fstest/test_all/config.yaml +++ b/fstest/test_all/config.yaml @@ -484,6 +484,9 @@ backends: - backend: "smb" remote: "TestSMB:rclone" fastlist: false + - backend: "smb" + remote: "TestSMBKerberos:rclone" + fastlist: false - backend: "storj" remote: "TestStorj:" fastlist: true diff --git a/fstest/testserver/init.d/PORTS.md b/fstest/testserver/init.d/PORTS.md index 5d34fb612..f599f6fd6 100644 --- a/fstest/testserver/init.d/PORTS.md +++ b/fstest/testserver/init.d/PORTS.md @@ -26,6 +26,8 @@ They should be bound to localhost so they are not accessible externally. | 28630 | TestSMB | | 28631 | TestFTPProftpd | | 28632 | TestSwiftAIOsegments | +| 28633 | TestSMBKerberos | +| 28634 | TestSMBKerberos | | 38081 | TestWebdavOwncloud | ## Non localhost tests diff --git a/fstest/testserver/init.d/TestSMBKerberos b/fstest/testserver/init.d/TestSMBKerberos new file mode 100755 index 000000000..3fa7e120c --- /dev/null +++ b/fstest/testserver/init.d/TestSMBKerberos @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +set -e + +IMAGE=rclone/test-smb-kerberos +NAME=smb-kerberos +USER=rclone +DOMAIN=RCLONE +REALM=RCLONE.LOCAL +SMB_PORT=28633 +KRB5_PORT=28634 + +. $(dirname "$0")/docker.bash + +start() { + docker build -t ${IMAGE} - <> /etc/samba/smb.conf +[public] +path = /share +browseable = yes +read only = yes +guest ok = yes +[rclone] +path = /rclone +browseable = yes +read only = no +guest ok = no +valid users = rclone +EOS +CMD ["samba", "-i"] +EOF + + docker run --rm -d --name ${NAME} \ + -p 127.0.0.1:${SMB_PORT}:445 \ + -p 127.0.0.1:${SMB_PORT}:445/udp \ + -p 127.0.0.1:${KRB5_PORT}:88 \ + ${IMAGE} + + # KRB5_CONFIG and KRB5CCNAME are set by the caller + cat > ${KRB5_CONFIG} <