Skip to content

Commit d9aa489

Browse files
committed
Adding support for sec=krb5 mounting
When mounting with kerberos security, ticket cache is expected to be set up on the host, pointing to the /var/lib/kubelet/kubernetes/krb5cc_${uid}. Credential cache is then taken from the creds secret and written to the file, that is available to the host for using.
1 parent 91c55ab commit d9aa489

File tree

5 files changed

+321
-1
lines changed

5 files changed

+321
-1
lines changed
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
apiVersion: storage.k8s.io/v1
3+
kind: StorageClass
4+
metadata:
5+
name: smb-krb5
6+
provisioner: smb.csi.k8s.io
7+
parameters:
8+
# On Windows, "*.default.svc.cluster.local" could not be recognized by csi-proxy
9+
source: "//smb-server.default.svc.cluster.local/share"
10+
# if csi.storage.k8s.io/provisioner-secret is provided, will create a sub directory
11+
# with PV name under source
12+
csi.storage.k8s.io/provisioner-secret-name: "smbcreds-krb5"
13+
csi.storage.k8s.io/provisioner-secret-namespace: "default"
14+
csi.storage.k8s.io/node-stage-secret-name: "smbcreds-krb5"
15+
csi.storage.k8s.io/node-stage-secret-namespace: "default"
16+
volumeBindingMode: Immediate
17+
mountOptions:
18+
- sec=krb5
19+
- cruid=1000
20+
- seal
21+
- vers=3.0
22+
- nosuid
23+
- noexec
24+
- dir_mode=0777
25+
- file_mode=0777
26+
- uid=1001
27+
- gid=1001
28+
- noperm
29+
- mfsymlinks
30+
- cache=strict
31+
- noserverino # required to prevent data corruption

docs/driver-parameters.md

+39
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,45 @@ nodeStageSecretRef.namespace | namespace where the secret is | k8s namespace |
3434
kubectl create secret generic smbcreds --from-literal username=USERNAME --from-literal password="PASSWORD"
3535
```
3636

37+
### Kerberos ticket support for Linux
38+
39+
40+
41+
42+
#### These are the conditions that must be met:
43+
- Kerberos support should be set up and cifs-utils must be installed on every node.
44+
- The directory /var/lib/kubelet/kerberos/ needs to exist, and it will hold kerberos credential cache files for various users.
45+
- This directory is shared between the host and the smb container.
46+
- The admin is responsible for cleaning up the directory on each node as they deem appropriate. It's important to note that unmounting doesn't delete the cache file.
47+
- Each node should know to look up in that directory, here's example script for that, expected to be run on node provision:
48+
```console
49+
mkdir -p /etc/krb5.conf.d/
50+
echo "[libdefaults]
51+
default_ccache_name = FILE:/var/lib/kubelet/kerberos/krb5cc_%{uid}" > /etc/krb5.conf.d/ccache.conf
52+
```
53+
- Mount flags should include **sec=krb5,cruid=1000**
54+
- sec=krb5 enables using credential cache
55+
- cruid=1000 provides information for what user credential cache will be looked up. This should match the secret entry.
56+
57+
#### Pass kerberos ticket in kubernetes secret
58+
To pass a ticket through secret, it needs to be acquired. Here's example how it can be done:
59+
60+
```console
61+
export KRB5CCNAME=/tmp/ccache # Use temporary file for the cache
62+
kinit USERNAME # Log in into domain
63+
kvno cifs/lowercase_server_name # Acquire ticket for the needed share, it'll be written to the cache file
64+
CCACHE=$(base64 -w 0 $KRB5CCNAME) # Get Base64-encoded cache
65+
```
66+
67+
And passing the actual ticket to the secret, instead of the password.
68+
Note that key for the ticket has included credential id, that must match exactly `cruid=` mount flag.
69+
In theory, nothing prevents from having more than single ticket cache in the same secret.
70+
```console
71+
kubectl create secret generic smbcreds-krb5 --from-literal krb5cc_1000=$CCACHE
72+
```
73+
74+
> See example of the [StorageClass](../deploy/example/storageclass-smb-krb5.yaml)
75+
3776
### Tips
3877
#### `subDir` parameter supports following pv/pvc metadata conversion
3978
> if `subDir` value contains following string, it would be converted into corresponding pv/pvc name or namespace

pkg/smb/nodeserver.go

+92-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ limitations under the License.
1717
package smb
1818

1919
import (
20+
"encoding/base64"
2021
"fmt"
2122
"os"
2223
"path/filepath"
2324
"runtime"
25+
"strconv"
2426
"strings"
2527
"time"
2628

@@ -182,10 +184,14 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe
182184
sensitiveMountOptions = []string{password}
183185
}
184186
} else {
187+
var useKerberosCache, err = ensureKerberosCache(mountFlags, secrets)
188+
if err != nil {
189+
return nil, status.Error(codes.Internal, fmt.Sprintf("Error writing kerberos cache: %v", err))
190+
}
185191
if err := os.MkdirAll(targetPath, 0750); err != nil {
186192
return nil, status.Error(codes.Internal, fmt.Sprintf("MkdirAll %s failed with error: %v", targetPath, err))
187193
}
188-
if requireUsernamePwdOption {
194+
if requireUsernamePwdOption && !useKerberosCache {
189195
sensitiveMountOptions = []string{fmt.Sprintf("%s=%s,%s=%s", usernameField, username, passwordField, password)}
190196
}
191197
mountOptions = mountFlags
@@ -422,3 +428,88 @@ func checkGidPresentInMountFlags(mountFlags []string) bool {
422428
}
423429
return false
424430
}
431+
432+
func hasKerberosMountOption(mountFlags []string) bool {
433+
for _, mountFlag := range mountFlags {
434+
if strings.HasPrefix(mountFlag, "sec=krb5") {
435+
return true
436+
}
437+
}
438+
return false
439+
}
440+
441+
func getCredUID(mountFlags []string) (int, error) {
442+
var cruidPrefix = "cruid="
443+
for _, mountFlag := range mountFlags {
444+
if strings.HasPrefix(mountFlag, cruidPrefix) {
445+
return strconv.Atoi(strings.TrimPrefix(mountFlag, cruidPrefix))
446+
}
447+
}
448+
return -1, fmt.Errorf("Can't find credUid in mount flags")
449+
}
450+
451+
func getKrb5CcacheName(credUID int) string {
452+
return fmt.Sprintf("%s%d", krb5Prefix, credUID)
453+
}
454+
455+
func getKrb5CacheFileName(credUID int) string {
456+
return fmt.Sprintf("%s%s%d", krb5CacheDirectory, krb5Prefix, credUID)
457+
}
458+
func kerberosCacheDirectoryExists() (bool, error) {
459+
_, err := os.Stat(krb5CacheDirectory)
460+
if os.IsNotExist(err) {
461+
return false, status.Error(codes.Internal, fmt.Sprintf("Directory for kerberos caches must exist, it will not be created: %s: %v", krb5CacheDirectory, err))
462+
} else if err != nil {
463+
return false, err
464+
}
465+
return true, nil
466+
}
467+
468+
func getKerberosCache(credUID int, secrets map[string]string) (string, []byte, error) {
469+
var krb5CcacheName = getKrb5CcacheName(credUID)
470+
var krb5CcacheContent string
471+
for k, v := range secrets {
472+
switch strings.ToLower(k) {
473+
case krb5CcacheName:
474+
krb5CcacheContent = v
475+
}
476+
}
477+
if krb5CcacheContent == "" {
478+
return "", nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Empty kerberos cache in key %s", krb5CcacheName))
479+
}
480+
content, err := base64.StdEncoding.DecodeString(krb5CcacheContent)
481+
if err != nil {
482+
return "", nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Malformed kerberos cache in key %s, expected to be in base64 form: %v", krb5CcacheName, err))
483+
}
484+
var krb5CacheFileName = getKrb5CacheFileName(credUID)
485+
486+
return krb5CacheFileName, content, nil
487+
}
488+
489+
func ensureKerberosCache(mountFlags []string, secrets map[string]string) (bool, error) {
490+
var securityIsKerberos = hasKerberosMountOption(mountFlags)
491+
if securityIsKerberos {
492+
_, err := kerberosCacheDirectoryExists()
493+
if err != nil {
494+
return false, err
495+
}
496+
credUID, err := getCredUID(mountFlags)
497+
if err != nil {
498+
return false, err
499+
}
500+
krb5CacheFileName, content, err := getKerberosCache(credUID, secrets)
501+
if err != nil {
502+
return false, err
503+
}
504+
err = os.WriteFile(krb5CacheFileName, content, os.FileMode(0700))
505+
if err != nil {
506+
return false, status.Error(codes.Internal, fmt.Sprintf("Couldn't write kerberos cache to file %s: %v", krb5CacheFileName, err))
507+
}
508+
err = os.Chown(krb5CacheFileName, credUID, credUID)
509+
if err != nil {
510+
return false, status.Error(codes.Internal, fmt.Sprintf("Couldn't chown kerberos cache %s to user %d: %v", krb5CacheFileName, credUID, err))
511+
}
512+
return true, nil
513+
}
514+
return false, nil
515+
}

pkg/smb/nodeserver_test.go

+157
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ package smb
1818

1919
import (
2020
"context"
21+
"encoding/base64"
2122
"errors"
2223
"fmt"
2324
"os"
2425
"path/filepath"
2526
"reflect"
2627
"runtime"
28+
"strconv"
2729
"strings"
2830
"syscall"
2931
"testing"
@@ -690,6 +692,161 @@ func TestCheckGidPresentInMountFlags(t *testing.T) {
690692
}
691693
}
692694

695+
func TestHasKerberosMountOption(t *testing.T) {
696+
tests := []struct {
697+
desc string
698+
MountFlags []string
699+
result bool
700+
}{
701+
{
702+
desc: "[Success] Sec kerberos present in mount flags",
703+
MountFlags: []string{"sec=krb5"},
704+
result: true,
705+
},
706+
{
707+
desc: "[Success] Sec kerberos present in mount flags",
708+
MountFlags: []string{"sec=krb5i"},
709+
result: true,
710+
},
711+
{
712+
desc: "[Success] Sec kerberos not present in mount flags",
713+
MountFlags: []string{},
714+
result: false,
715+
},
716+
{
717+
desc: "[Success] Sec kerberos not present in mount flags",
718+
MountFlags: []string{"sec=ntlm"},
719+
result: false,
720+
},
721+
}
722+
723+
for _, test := range tests {
724+
securityIsKerberos := hasKerberosMountOption(test.MountFlags)
725+
if securityIsKerberos != test.result {
726+
t.Errorf("[%s]: Expected result : %t, Actual result: %t", test.desc, test.result, securityIsKerberos)
727+
}
728+
}
729+
}
730+
731+
func TestGetCredUID(t *testing.T) {
732+
_, convertErr := strconv.Atoi("foo")
733+
tests := []struct {
734+
desc string
735+
MountFlags []string
736+
result int
737+
expectedErr error
738+
}{
739+
{
740+
desc: "[Success] Got correct credUID",
741+
MountFlags: []string{"cruid=1000"},
742+
result: 1000,
743+
expectedErr: nil,
744+
},
745+
{
746+
desc: "[Success] Got correct credUID",
747+
MountFlags: []string{"cruid=0"},
748+
result: 0,
749+
expectedErr: nil,
750+
},
751+
{
752+
desc: "[Error] Got error when no CredUID",
753+
MountFlags: []string{},
754+
result: -1,
755+
expectedErr: fmt.Errorf("Can't find credUid in mount flags"),
756+
},
757+
{
758+
desc: "[Error] Got error when CredUID is not an int",
759+
MountFlags: []string{"cruid=foo"},
760+
result: 0,
761+
expectedErr: convertErr,
762+
},
763+
}
764+
765+
for _, test := range tests {
766+
credUID, err := getCredUID(test.MountFlags)
767+
if credUID != test.result {
768+
t.Errorf("[%s]: Expected result : %d, Actual result: %d", test.desc, test.result, credUID)
769+
}
770+
if !reflect.DeepEqual(err, test.expectedErr) {
771+
t.Errorf("[%s]: Expected error : %v, Actual error: %v", test.desc, test.expectedErr, err)
772+
}
773+
}
774+
}
775+
776+
func TestGetKerberosCache(t *testing.T) {
777+
ticket := []byte{'G', 'O', 'L', 'A', 'N', 'G'}
778+
base64Ticket := base64.StdEncoding.EncodeToString(ticket)
779+
credUID := 1000
780+
goodFileName := fmt.Sprintf("%s%s%d", krb5CacheDirectory, krb5Prefix, credUID)
781+
krb5CcacheName := "krb5cc_1000"
782+
783+
_, base64DecError := base64.StdEncoding.DecodeString("123")
784+
tests := []struct {
785+
desc string
786+
credUID int
787+
secrets map[string]string
788+
expectedFileName string
789+
expectedContent []byte
790+
expectedErr error
791+
}{
792+
{
793+
desc: "[Success] Got correct filename and content",
794+
credUID: 1000,
795+
secrets: map[string]string{
796+
krb5CcacheName: base64Ticket,
797+
},
798+
expectedFileName: goodFileName,
799+
expectedContent: ticket,
800+
expectedErr: nil,
801+
},
802+
{
803+
desc: "[Error] Throw error if credUID mismatch",
804+
credUID: 1001,
805+
secrets: map[string]string{
806+
krb5CcacheName: base64Ticket,
807+
},
808+
expectedFileName: "",
809+
expectedContent: nil,
810+
expectedErr: status.Error(codes.InvalidArgument, fmt.Sprintf("Empty kerberos cache in key %s", "krb5cc_1001")),
811+
},
812+
{
813+
desc: "[Error] Throw error if ticket is empty in secret",
814+
credUID: 1000,
815+
secrets: map[string]string{
816+
krb5CcacheName: "",
817+
},
818+
expectedFileName: "",
819+
expectedContent: nil,
820+
expectedErr: status.Error(codes.InvalidArgument, fmt.Sprintf("Empty kerberos cache in key %s", krb5CcacheName)),
821+
},
822+
{
823+
desc: "[Error] Throw error if ticket is invalid base64",
824+
credUID: 1000,
825+
secrets: map[string]string{
826+
krb5CcacheName: "123",
827+
},
828+
expectedFileName: "",
829+
expectedContent: nil,
830+
expectedErr: status.Error(codes.InvalidArgument, fmt.Sprintf("Malformed kerberos cache in key %s, expected to be in base64 form: %v", krb5CcacheName, base64DecError)),
831+
},
832+
}
833+
834+
for _, test := range tests {
835+
fileName, content, err := getKerberosCache(test.credUID, test.secrets)
836+
if !reflect.DeepEqual(err, test.expectedErr) {
837+
t.Errorf("[%s]: Expected error : %v, Actual error: %v", test.desc, test.expectedErr, err)
838+
} else {
839+
if fileName != test.expectedFileName {
840+
t.Errorf("[%s]: Expected filename : %s, Actual result: %s", test.desc, test.expectedFileName, fileName)
841+
}
842+
if !reflect.DeepEqual(content, test.expectedContent) {
843+
t.Errorf("[%s]: Expected content : %s, Actual content: %s", test.desc, test.expectedContent, content)
844+
}
845+
}
846+
}
847+
848+
}
849+
693850
func TestNodePublishVolumeIdempotentMount(t *testing.T) {
694851
if runtime.GOOS == "windows" || os.Getuid() != 0 {
695852
return

pkg/smb/smb.go

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const (
3535
sourceField = "source"
3636
subDirField = "subdir"
3737
domainField = "domain"
38+
krb5Prefix = "krb5cc_"
39+
krb5CacheDirectory = "/var/lib/kubelet/kerberos/"
3840
mountOptionsField = "mountoptions"
3941
defaultDomainName = "AZURE"
4042
pvcNameKey = "csi.storage.k8s.io/pvc/name"

0 commit comments

Comments
 (0)