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..8fb20d21 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,6 +634,7 @@ 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 encrypted home dataset": {def: "m_with_userdata.yaml", encryptHome: true}, // Second pool cases "User dataset on other pool": {def: "m_with_userdata_on_other_pool.yaml"}, @@ -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/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 {