From 7d27ce81bb63004a47580982a7a0add93e94b4d2 Mon Sep 17 00:00:00 2001 From: Crag Wang Date: Tue, 6 Oct 2020 15:34:16 +0800 Subject: [PATCH] zsysctl: add flag to support creating an encrypted dataset Create an encrypted dataset with raw type keyfile when particular flag is given to userdata create. For example: $ zsysctl userdata create -e myuser /home/myuser Signed-off-by: Crag Wang --- cmd/zsysd/client/userdata.go | 10 ++++++---- internal/daemon/userdata.go | 3 ++- internal/machines/machines_test.go | 14 ++++++++------ internal/machines/userdata.go | 10 +++++++--- internal/zfs/libzfs/adapter.go | 6 ++++++ internal/zfs/zfs.go | 25 +++++++++++++++++++++++-- internal/zfs/zfs_test.go | 5 +++-- zsys.pb.go | 8 ++++++++ zsys.proto | 1 + 9 files changed, 64 insertions(+), 18 deletions(-) diff --git a/cmd/zsysd/client/userdata.go b/cmd/zsysd/client/userdata.go index b96a2fb4..5b13ec9a 100644 --- a/cmd/zsysd/client/userdata.go +++ b/cmd/zsysd/client/userdata.go @@ -23,7 +23,7 @@ var ( Use: "create USER HOME_DIRECTORY", Short: i18n.G("Create a new home user dataset via an user dataset (if doesn't exist) creation"), Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { cmdErr = createUserData(args[0], args[1]) }, + Run: func(cmd *cobra.Command, args []string) { cmdErr = createUserData(args[0], args[1], encryptHome) }, } userdataRenameCmd = &cobra.Command{ Use: "set-home OLD_HOME NEW_HOME", @@ -40,7 +40,8 @@ var ( ) var ( - removeHome bool + removeHome bool + encryptHome bool ) func init() { @@ -48,12 +49,13 @@ func init() { userdataCmd.AddCommand(userdataCreateCmd) userdataCmd.AddCommand(userdataRenameCmd) userdataCmd.AddCommand(userdataDissociateCmd) + userdataCreateCmd.Flags().BoolVarP(&encryptHome, "encrypt", "e", false, i18n.G("Encrypt home directory in associatation with keyfile")) userdataDissociateCmd.Flags().BoolVarP(&removeHome, "remove", "r", false, i18n.G("Empty home directory content if not associated to any machine state")) } // createUserData creates a new userdata for user and set it to homepath on current zsys system. // if the user already exists for a dataset attached to the current system, set its mountpoint to homepath. -func createUserData(user, homepath string) (err error) { +func createUserData(user, homepath string, encryptHome bool) (err error) { client, err := newClient() if err != nil { return err @@ -63,7 +65,7 @@ func createUserData(user, homepath string) (err error) { ctx, cancel, reset := contextWithResettableTimeout(client.Ctx, config.DefaultClientTimeout) defer cancel() - stream, err := client.CreateUserData(ctx, &zsys.CreateUserDataRequest{User: user, Homepath: homepath}) + stream, err := client.CreateUserData(ctx, &zsys.CreateUserDataRequest{User: user, Homepath: homepath, EncryptHome: encryptHome}) if err = checkConn(err, reset); err != nil { return err } diff --git a/internal/daemon/userdata.go b/internal/daemon/userdata.go index 392ca8c2..3c4e91c0 100644 --- a/internal/daemon/userdata.go +++ b/internal/daemon/userdata.go @@ -19,13 +19,14 @@ func (s *Server) CreateUserData(req *zsys.CreateUserDataRequest, stream zsys.Zsy } user := req.GetUser() + encryptHome := req.GetEncryptHome() homepath := req.GetHomepath() s.RWRequest.Lock() defer s.RWRequest.Unlock() log.Infof(stream.Context(), i18n.G("Create user dataset for %q on %q"), user, homepath) - if err := s.Machines.CreateUserData(stream.Context(), user, homepath); err != nil { + if err := s.Machines.CreateUserData(stream.Context(), user, homepath, encryptHome); err != nil { return fmt.Errorf(i18n.G("couldn't create userdataset for %q: ")+config.ErrorFormat, homepath, err) } return nil diff --git a/internal/machines/machines_test.go b/internal/machines/machines_test.go index d3b1cfe9..01527cda 100644 --- a/internal/machines/machines_test.go +++ b/internal/machines/machines_test.go @@ -617,10 +617,11 @@ func TestUpdateLastUsed(t *testing.T) { func TestCreateUserData(t *testing.T) { t.Parallel() tests := map[string]struct { - def string - user string - homePath string - cmdline string + def string + user string + homePath string + encryptHome bool + cmdline string setPropertyErr bool createErr bool @@ -633,12 +634,13 @@ func TestCreateUserData(t *testing.T) { "One machine add user dataset without userdata": {def: "m_without_userdata.yaml"}, "One machine with no user, only userdata": {def: "m_with_userdata_only.yaml"}, "No attached userdata": {def: "m_no_attached_userdata_first_pool.yaml"}, + "One machine add user dataset with encryption": {def: "m_with_userdata.yaml", encryptHome: true}, // Second pool cases "User dataset on other pool": {def: "m_with_userdata_on_other_pool.yaml"}, "User dataset with no user on other pool": {def: "m_with_userdata_only_on_other_pool.yaml"}, "Prefer system pool for userdata": {def: "m_without_userdata_prefer_system_pool.yaml"}, - "Prefer system pool (try other pool) for userdata": {def: "m_without_userdata_prefer_system_pool.yaml", cmdline: generateCmdLine("rpool2/ROOT/ubuntu_1234")}, + "Prefer system pool (try other pool) for userdata": {def: "m_without_userdata_prefer_system_pool.yaml", cmdline: generateCmdLine("rpool2/ROOT/ubuntu_1234"), , encryptHome: false}, "No attached userdata on second pool": {def: "m_no_attached_userdata_second_pool.yaml"}, // User or home edge cases @@ -685,7 +687,7 @@ func TestCreateUserData(t *testing.T) { lzfs.ErrOnScan(tc.scanErr) lzfs.ErrOnSetProperty(tc.setPropertyErr) - err = ms.CreateUserData(context.Background(), getDefaultValue(tc.user, "userfoo"), getDefaultValue(tc.homePath, "/home/foo")) + err = ms.CreateUserData(context.Background(), getDefaultValue(tc.user, "userfoo"), getDefaultValue(tc.homePath, "/home/foo"), tc.encryptHome) if err != nil { if !tc.wantErr { t.Fatalf("expected no error but got: %v", err) diff --git a/internal/machines/userdata.go b/internal/machines/userdata.go index 6c711656..1f6dbbf6 100644 --- a/internal/machines/userdata.go +++ b/internal/machines/userdata.go @@ -22,7 +22,7 @@ import ( // CreateUserData creates a new dataset for homepath and attach to current system. // It creates intermediates user datasets if needed. -func (ms *Machines) CreateUserData(ctx context.Context, user, homepath string) error { +func (ms *Machines) CreateUserData(ctx context.Context, user, homepath string, encryptHome bool) error { if !ms.current.isZsys() { return errors.New(i18n.G("Current machine isn't Zsys, nothing to create")) } @@ -81,14 +81,18 @@ selectUserDataset: userdatasetRoot = filepath.Join(p, zfs.UserdataPrefix) // Create parent USERDATA - if err := t.Create(userdatasetRoot, "/", "off"); err != nil { + if err := t.Create(userdatasetRoot, "/", "off", false); err != nil { cancel() return fmt.Errorf(i18n.G("couldn't create user data embedder dataset: ")+config.ErrorFormat, err) } } userdataset := filepath.Join(userdatasetRoot, fmt.Sprintf("%s_%s", user, t.Zfs.GenerateID(6))) - if err := t.Create(userdataset, homepath, "on"); err != nil { + var canmountProp string = "on" + if encryptHome { + canmountProp = "noauto" + } + if err := t.Create(userdataset, homepath, canmountProp, encryptHome); err != nil { cancel() return err } diff --git a/internal/zfs/libzfs/adapter.go b/internal/zfs/libzfs/adapter.go index f8320025..86b139c3 100644 --- a/internal/zfs/libzfs/adapter.go +++ b/internal/zfs/libzfs/adapter.go @@ -47,6 +47,12 @@ const ( DatasetPropCanmount = golibzfs.DatasetPropCanmount // DatasetPropMountpoint is the mountpoint of the dataset DatasetPropMountpoint = golibzfs.DatasetPropMountpoint + // DatasetPropEncryption is the encryption of the dataste + DatasetPropEncryption = golibzfs.DatasetPropEncryption + // DatasetPropEncryption is the encryption of the dataste + DatasetPropKeyLocation = golibzfs.DatasetPropKeyLocation + // DatasetPropEncryption is the encryption of the dataste + DatasetPropKeyFormat = golibzfs.DatasetPropKeyFormat // DatasetPropOrigin is the origin of the dataset DatasetPropOrigin = golibzfs.DatasetPropOrigin // DatasetPropMounted is the mounted property for the dataset diff --git a/internal/zfs/zfs.go b/internal/zfs/zfs.go index 1fabdfea..690282f5 100644 --- a/internal/zfs/zfs.go +++ b/internal/zfs/zfs.go @@ -2,7 +2,10 @@ package zfs import ( "context" + "crypto/rand" "fmt" + "io" + "io/ioutil" "path/filepath" "strings" @@ -315,16 +318,34 @@ func (t *nestedTransaction) Done(err *error) { t.parent.reverts = append(t.parent.reverts, t.reverts...) } +func genRandBytes(keyLength int) []byte { + keytext := make([]byte, keyLength) + if _, err := io.ReadFull(rand.Reader, keytext); err != nil { + panic(err.Error()) + } + return keytext +} + // Create creates a dataset for that path. -func (t *Transaction) Create(path, mountpoint, canmount string) error { +func (t *Transaction) Create(path, mountpoint, canmount string, encryption bool) error { t.checkValid() - log.Debugf(t.ctx, i18n.G("ZFS: trying to Create %q with mountpoint %q"), path, mountpoint) + log.Debugf(t.ctx, i18n.G("ZFS: trying to Create %q with mountpoint %q; encryption=%q"), path, mountpoint, fmt.Sprint(encryption)) props := make(map[libzfs.Prop]libzfs.Property) if mountpoint != "" { props[libzfs.DatasetPropMountpoint] = libzfs.Property{Value: mountpoint} } + if encryption { + var keyfilePath string = filepath.Join(filepath.Dir(mountpoint), filepath.Base(mountpoint)+".keyfile") + if err := ioutil.WriteFile(keyfilePath, genRandBytes(32), 0400); err != nil { + panic(err.Error()) + } + + props[libzfs.DatasetPropEncryption] = libzfs.Property{Value: "aes-256-gcm"} + props[libzfs.DatasetPropKeyLocation] = libzfs.Property{Value: "file://" + keyfilePath} + props[libzfs.DatasetPropKeyFormat] = libzfs.Property{Value: "raw"} + } props[libzfs.DatasetPropCanmount] = libzfs.Property{Value: canmount} dZFS, err := t.Zfs.libzfs.DatasetCreate(path, libzfs.DatasetTypeFilesystem, props) diff --git a/internal/zfs/zfs_test.go b/internal/zfs/zfs_test.go index e533872e..a9422ca9 100644 --- a/internal/zfs/zfs_test.go +++ b/internal/zfs/zfs_test.go @@ -163,6 +163,7 @@ func TestCreate(t *testing.T) { path string mountpoint string canmount string + encryption bool wantErr bool }{ @@ -194,7 +195,7 @@ func TestCreate(t *testing.T) { trans, _ := z.NewTransaction(context.Background()) defer trans.Done() - err = trans.Create(tc.path, tc.mountpoint, tc.canmount) + err = trans.Create(tc.path, tc.mountpoint, tc.canmount, tc.encryption) if err != nil && !tc.wantErr { t.Fatalf("expected no error but got: %v", err) @@ -976,7 +977,7 @@ func TestTransactionsWithZFS(t *testing.T) { // create a dataset without its parent will make it fail datasetName = "rpool/ROOT/ubuntu_4242/opt" } - err := trans.Create(datasetName, "/home/foo", "on") + err := trans.Create(datasetName, "/home/foo", "on", false) if !tc.shouldErr && err != nil { t.Fatalf("create %q shouldn't have failed but it did: %v", datasetName, err) } else if tc.shouldErr && err == nil { diff --git a/zsys.pb.go b/zsys.pb.go index a580299e..82a2c8e4 100644 --- a/zsys.pb.go +++ b/zsys.pb.go @@ -177,6 +177,7 @@ func (*VersionResponse) XXX_OneofWrappers() []interface{} { type CreateUserDataRequest struct { User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` Homepath string `protobuf:"bytes,2,opt,name=homepath,proto3" json:"homepath,omitempty"` + EncryptHome bool `protobuf:"varint,3,opt,name=encryptHome,proto3" json:"encryptHome,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -214,6 +215,13 @@ func (m *CreateUserDataRequest) GetUser() string { return "" } +func (m *CreateUserDataRequest) GetEncryptHome() bool { + if m != nil { + return m.EncryptHome + } + return false +} + func (m *CreateUserDataRequest) GetHomepath() string { if m != nil { return m.Homepath diff --git a/zsys.proto b/zsys.proto index c0678895..702baeb2 100644 --- a/zsys.proto +++ b/zsys.proto @@ -47,6 +47,7 @@ message VersionResponse { message CreateUserDataRequest { string user = 1; string homepath = 2; + bool encryptHome = 3; } message ChangeHomeOnUserDataRequest {