mirror of
https://github.com/octoleo/restic.git
synced 2024-11-25 06:07:44 +00:00
parent
71c653f9e0
commit
f31b4f29c1
21
changelog/unreleased/pull-3419
Normal file
21
changelog/unreleased/pull-3419
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
Enhancement: Use config file permissions to control file group access
|
||||||
|
|
||||||
|
Previously files in a local/sftp restic repository would always end up with
|
||||||
|
very restrictive access permissions allowing access only to the owner. This
|
||||||
|
prevented a number of valid use-cases involving groups and ACLs.
|
||||||
|
|
||||||
|
Now we use the config file permissions to decide whether group access
|
||||||
|
should be given to newly created repository files or not. We arrange for
|
||||||
|
repository files to be created group readable exactly when the repository
|
||||||
|
config file is group readable.
|
||||||
|
|
||||||
|
To opt-in to group readable repositories a simple `chmod -R g+r` or
|
||||||
|
equivalent can be used. For repositories that should be writable by group
|
||||||
|
members a tad more setup is required, see the docs.
|
||||||
|
|
||||||
|
Posix ACLs can also be used now that the group permissions being forced to
|
||||||
|
zero no longer masks the effect of ACL entries.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2351
|
||||||
|
https://github.com/restic/restic/pull/3419
|
||||||
|
https://forum.restic.net/t/change-permissions-on-repository-files/1391
|
@ -699,3 +699,56 @@ On MSYS2, you can install ``winpty`` as follows:
|
|||||||
|
|
||||||
$ pacman -S winpty
|
$ pacman -S winpty
|
||||||
$ winpty restic -r /srv/restic-repo init
|
$ winpty restic -r /srv/restic-repo init
|
||||||
|
|
||||||
|
|
||||||
|
Group accessible repositories
|
||||||
|
*****************************
|
||||||
|
|
||||||
|
Since restic version 0.14 local and SFTP repositories can be made
|
||||||
|
accessible to members of a system group. To control this we have to change
|
||||||
|
the group permissions of the top-level ``config`` file and restic will use
|
||||||
|
this as a hint to determine what permissions to apply to newly created
|
||||||
|
files. By default ``restic init`` sets repositories up to be group
|
||||||
|
inaccessible.
|
||||||
|
|
||||||
|
In order to give group members read-only access we simply add the read
|
||||||
|
permission bit to all repository files with ``chmod``:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ chmod -R g+r /srv/restic-repo
|
||||||
|
|
||||||
|
This serves two purposes: 1) it sets the read permission bit on the
|
||||||
|
repository config file triggering restic's logic to create new files as
|
||||||
|
group accessible and 2) it actually allows the group read access to the
|
||||||
|
files.
|
||||||
|
|
||||||
|
.. note:: By default files on Unix systems are created with a user's
|
||||||
|
primary group as defined by the gid (group id) field in
|
||||||
|
``/etc/passwd``. See `passwd(5)
|
||||||
|
<https://manpages.debian.org/latest/passwd/passwd.5.en.html>`_.
|
||||||
|
|
||||||
|
For read-write access things are a bit more complicated. When users other
|
||||||
|
than the repository creator add new files in the repository they will be
|
||||||
|
group-owned by this user's primary group by default, not that of the
|
||||||
|
original repository owner, meaning the original creator wouldn't have
|
||||||
|
access to these files. That's hardly what you'd want.
|
||||||
|
|
||||||
|
To make this work we can employ the help of the ``setgid`` permission bit
|
||||||
|
available on Linux and most other Unix systems. This permission bit makes
|
||||||
|
newly created directories inherit both the group owner (gid) and setgid bit
|
||||||
|
from the parent directory. Setting this bit requires root but since it
|
||||||
|
propagates down to any new directories we only have to do this priviledged
|
||||||
|
setup once:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# find /srv/restic-repo -type d -exec chmod g+s '{}' \;
|
||||||
|
$ chmod -R g+rw /srv/restic-repo
|
||||||
|
|
||||||
|
This sets the ``setgid`` bit on all existing directories in the repository
|
||||||
|
and then grants read/write permissions for group access.
|
||||||
|
|
||||||
|
.. note:: To manage who has access to the repository you can use
|
||||||
|
``usermod`` on Linux systems, to change which group controls
|
||||||
|
repository access ``chgrp -R`` is your friend.
|
||||||
|
@ -24,6 +24,7 @@ type Local struct {
|
|||||||
Config
|
Config
|
||||||
sem *backend.Semaphore
|
sem *backend.Semaphore
|
||||||
backend.Layout
|
backend.Layout
|
||||||
|
backend.Modes
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure statically that *Local implements restic.Backend.
|
// ensure statically that *Local implements restic.Backend.
|
||||||
@ -42,10 +43,15 @@ func open(ctx context.Context, cfg Config) (*Local, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fi, err := fs.Stat(l.Filename(restic.Handle{Type: restic.ConfigFile}))
|
||||||
|
m := backend.DeriveModesFromFileInfo(fi, err)
|
||||||
|
debug.Log("using (%03O file, %03O dir) permissions", m.File, m.Dir)
|
||||||
|
|
||||||
return &Local{
|
return &Local{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Layout: l,
|
Layout: l,
|
||||||
sem: sem,
|
sem: sem,
|
||||||
|
Modes: m,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +79,7 @@ func Create(ctx context.Context, cfg Config) (*Local, error) {
|
|||||||
|
|
||||||
// create paths for data and refs
|
// create paths for data and refs
|
||||||
for _, d := range be.Paths() {
|
for _, d := range be.Paths() {
|
||||||
err := fs.MkdirAll(d, backend.Modes.Dir)
|
err := fs.MkdirAll(d, be.Modes.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -129,7 +135,7 @@ func (b *Local) Save(ctx context.Context, h restic.Handle, rd restic.RewindReade
|
|||||||
debug.Log("error %v: creating dir", err)
|
debug.Log("error %v: creating dir", err)
|
||||||
|
|
||||||
// error is caused by a missing directory, try to create it
|
// error is caused by a missing directory, try to create it
|
||||||
mkdirErr := fs.MkdirAll(dir, backend.Modes.Dir)
|
mkdirErr := fs.MkdirAll(dir, b.Modes.Dir)
|
||||||
if mkdirErr != nil {
|
if mkdirErr != nil {
|
||||||
debug.Log("error creating dir %v: %v", dir, mkdirErr)
|
debug.Log("error creating dir %v: %v", dir, mkdirErr)
|
||||||
} else {
|
} else {
|
||||||
@ -189,7 +195,7 @@ func (b *Local) Save(ctx context.Context, h restic.Handle, rd restic.RewindReade
|
|||||||
// try to mark file as read-only to avoid accidential modifications
|
// try to mark file as read-only to avoid accidential modifications
|
||||||
// ignore if the operation fails as some filesystems don't allow the chmod call
|
// ignore if the operation fails as some filesystems don't allow the chmod call
|
||||||
// e.g. exfat and network file systems with certain mount options
|
// e.g. exfat and network file systems with certain mount options
|
||||||
err = setFileReadonly(finalname, backend.Modes.File)
|
err = setFileReadonly(finalname, b.Modes.File)
|
||||||
if err != nil && !os.IsPermission(err) {
|
if err != nil && !os.IsPermission(err) {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,28 @@ var Paths = struct {
|
|||||||
"config",
|
"config",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modes holds the default modes for directories and files for file-based
|
type Modes struct {
|
||||||
// backends.
|
Dir os.FileMode
|
||||||
var Modes = struct{ Dir, File os.FileMode }{0700, 0600}
|
File os.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultModes defines the default permissions to apply to new repository
|
||||||
|
// files and directories stored on file-based backends.
|
||||||
|
var DefaultModes = Modes{Dir: 0700, File: 0600}
|
||||||
|
|
||||||
|
// DeriveModesFromFileInfo will, given the mode of a regular file, compute
|
||||||
|
// the mode we should use for new files and directories. If the passed
|
||||||
|
// error is non-nil DefaultModes are returned.
|
||||||
|
func DeriveModesFromFileInfo(fi os.FileInfo, err error) Modes {
|
||||||
|
m := DefaultModes
|
||||||
|
if err != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode()&0040 != 0 { // Group has read access
|
||||||
|
m.Dir |= 0070
|
||||||
|
m.File |= 0060
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
@ -34,6 +34,7 @@ type SFTP struct {
|
|||||||
sem *backend.Semaphore
|
sem *backend.Semaphore
|
||||||
backend.Layout
|
backend.Layout
|
||||||
Config
|
Config
|
||||||
|
backend.Modes
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ restic.Backend = &SFTP{}
|
var _ restic.Backend = &SFTP{}
|
||||||
@ -140,9 +141,14 @@ func Open(ctx context.Context, cfg Config) (*SFTP, error) {
|
|||||||
|
|
||||||
debug.Log("layout: %v\n", sftp.Layout)
|
debug.Log("layout: %v\n", sftp.Layout)
|
||||||
|
|
||||||
|
fi, err := sftp.c.Stat(Join(cfg.Path, backend.Paths.Config))
|
||||||
|
m := backend.DeriveModesFromFileInfo(fi, err)
|
||||||
|
debug.Log("using (%03O file, %03O dir) permissions", m.File, m.Dir)
|
||||||
|
|
||||||
sftp.Config = cfg
|
sftp.Config = cfg
|
||||||
sftp.p = cfg.Path
|
sftp.p = cfg.Path
|
||||||
sftp.sem = sem
|
sftp.sem = sem
|
||||||
|
sftp.Modes = m
|
||||||
return sftp, nil
|
return sftp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +231,8 @@ func Create(ctx context.Context, cfg Config) (*SFTP, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sftp.Modes = backend.DefaultModes
|
||||||
|
|
||||||
// test if config file already exists
|
// test if config file already exists
|
||||||
_, err = sftp.c.Lstat(Join(cfg.Path, backend.Paths.Config))
|
_, err = sftp.c.Lstat(Join(cfg.Path, backend.Paths.Config))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -311,7 +319,7 @@ func (r *SFTP) Save(ctx context.Context, h restic.Handle, rd restic.RewindReader
|
|||||||
// pkg/sftp doesn't allow creating with a mode.
|
// pkg/sftp doesn't allow creating with a mode.
|
||||||
// Chmod while the file is still empty.
|
// Chmod while the file is still empty.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = f.Chmod(backend.Modes.File)
|
err = f.Chmod(r.Modes.File)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "OpenFile")
|
return errors.Wrap(err, "OpenFile")
|
||||||
|
Loading…
Reference in New Issue
Block a user