diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index 8dec6490..c13b8ea7 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -105,6 +105,7 @@ var ( disableACLs bool mpMaxParts int copyObjectThreshold int64 + socketPerm string ) var ( @@ -772,6 +773,12 @@ func initFlags() []cli.Flag { Value: 5 * 1024 * 1024 * 1024, Destination: ©ObjectThreshold, }, + &cli.StringFlag{ + Name: "socket-perm", + Usage: "file permissions for file-backed UNIX domain sockets (octal, e.g. '0660'); ignored for TCP/IP and abstract namespace sockets", + EnvVars: []string{"VGW_SOCKET_PERM"}, + Destination: &socketPerm, + }, } } @@ -855,6 +862,15 @@ func runGateway(ctx context.Context, be backend.Backend) error { utils.SetBucketNameValidationStrict(!disableStrictBucketNames) + var parsedSocketPerm os.FileMode + if socketPerm != "" { + perm, err := strconv.ParseUint(socketPerm, 8, 32) + if err != nil { + return fmt.Errorf("invalid --socket-perm value %q: must be an octal integer (e.g. '0660'): %w", socketPerm, err) + } + parsedSocketPerm = os.FileMode(perm) + } + if pprof != "" { // listen on specified port for pprof debug // point browser to http:///debug/pprof/ @@ -867,6 +883,9 @@ func runGateway(ctx context.Context, be backend.Backend) error { s3api.WithConcurrencyLimiter(maxConnections, maxRequests), s3api.WithMpMaxParts(mpMaxParts), } + if socketPerm != "" { + opts = append(opts, s3api.WithSocketPerm(parsedSocketPerm)) + } if corsAllowOrigin != "" { opts = append(opts, s3api.WithCORSAllowOrigin(corsAllowOrigin)) } @@ -1103,6 +1122,9 @@ func runGateway(ctx context.Context, be backend.Backend) error { if debug { opts = append(opts, s3api.WithAdminDebug()) } + if socketPerm != "" { + opts = append(opts, s3api.WithAdminSocketPerm(parsedSocketPerm)) + } admSrv = s3api.NewAdminServer(be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, region, iam, loggers.AdminLogger, srv.Router.Ctrl, opts...) } @@ -1215,6 +1237,9 @@ func runGateway(ctx context.Context, be backend.Backend) error { if webuiPathPrefix != "" { webOpts = append(webOpts, webui.WithPathPrefix(webuiPathPrefix)) } + if socketPerm != "" { + webOpts = append(webOpts, webui.WithSocketPerm(parsedSocketPerm)) + } webSrv = webui.NewServer(&webui.ServerConfig{ Gateways: gateways, diff --git a/extra/example.conf b/extra/example.conf index 2411c1ca..caf869b2 100644 --- a/extra/example.conf +++ b/extra/example.conf @@ -102,6 +102,14 @@ ROOT_SECRET_ACCESS_KEY= #VGW_ADMIN_CERT= #VGW_ADMIN_CERT_KEY= +# The VGW_SOCKET_PERM option sets the file-mode permissions for file-backed +# UNIX domain sockets created by the S3, admin, and WebUI servers. The value +# must be an octal integer (e.g. 0660 to allow owner and group read/write +# access). This option has no effect on TCP/IP addresses or Linux abstract +# namespace sockets (those prefixed with "@"). When not set, the socket file +# permissions are determined by the process umask. +#VGW_SOCKET_PERM= + # The VGW_QUIET option when set will supress the S3 server request summary # logging to stdout. #VGW_QUIET=false diff --git a/s3api/admin-server.go b/s3api/admin-server.go index b81a706b..e49cac91 100644 --- a/s3api/admin-server.go +++ b/s3api/admin-server.go @@ -17,6 +17,7 @@ package s3api import ( "fmt" "net" + "os" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" @@ -40,6 +41,7 @@ type S3AdminServer struct { corsAllowOrigin string maxConnections int maxRequests int + socketPerm os.FileMode } func NewAdminServer(be backend.Backend, root middlewares.RootUserConfig, region string, iam auth.IAMService, l s3log.AuditLogger, ctrl controllers.S3ApiController, opts ...AdminOpt) *S3AdminServer { @@ -124,6 +126,13 @@ func WithAdminConcurrencyLimiter(maxConnections, maxRequests int) AdminOpt { } } +// WithAdminSocketPerm sets the file-mode permissions applied to file-backed +// UNIX domain sockets after binding. It has no effect on TCP/IP or abstract +// namespace sockets. +func WithAdminSocketPerm(perm os.FileMode) AdminOpt { + return func(s *S3AdminServer) { s.socketPerm = perm } +} + // ServeMultiPort creates listeners for multiple port specifications and serves // on all of them simultaneously. This supports listening on multiple ports and/or // addresses (e.g., [":8080", "localhost:8081"]). @@ -140,9 +149,9 @@ func (sa *S3AdminServer) ServeMultiPort(ports []string) error { var err error if sa.CertStorage != nil { - ln, err = utils.NewMultiAddrTLSListener(sa.app.Config().Network, portSpec, sa.CertStorage.GetCertificate) + ln, err = utils.NewMultiAddrTLSListener(sa.app.Config().Network, portSpec, sa.CertStorage.GetCertificate, utils.ListenerOptions{SocketPerm: sa.socketPerm}) } else { - ln, err = utils.NewMultiAddrListener(sa.app.Config().Network, portSpec) + ln, err = utils.NewMultiAddrListener(sa.app.Config().Network, portSpec, utils.ListenerOptions{SocketPerm: sa.socketPerm}) } if err != nil { diff --git a/s3api/server.go b/s3api/server.go index 1865d677..fd903284 100644 --- a/s3api/server.go +++ b/s3api/server.go @@ -19,6 +19,7 @@ import ( "fmt" "net" "net/http" + "os" "strings" "time" @@ -54,6 +55,7 @@ type S3ApiServer struct { maxRequests int webuiMountPrefix string webuiSrvCfg *webui.ServerConfig + socketPerm os.FileMode } func New( @@ -217,6 +219,13 @@ func WithConcurrencyLimiter(maxConnections, maxRequests int) Option { } } +// WithSocketPerm sets the file-mode permissions applied to file-backed UNIX +// domain sockets after binding. It has no effect on TCP/IP or abstract +// namespace sockets. +func WithSocketPerm(perm os.FileMode) Option { + return func(s *S3ApiServer) { s.socketPerm = perm } +} + // WithDisableACL disables the s3 api server ACLs, by ignoring all // bucket/object ACL headers func WithDisableACL() Option { @@ -239,9 +248,9 @@ func (sa *S3ApiServer) ServeMultiPort(ports []string) error { var err error if sa.CertStorage != nil { - ln, err = utils.NewMultiAddrTLSListener(sa.app.Config().Network, portSpec, sa.CertStorage.GetCertificate) + ln, err = utils.NewMultiAddrTLSListener(sa.app.Config().Network, portSpec, sa.CertStorage.GetCertificate, utils.ListenerOptions{SocketPerm: sa.socketPerm}) } else { - ln, err = utils.NewMultiAddrListener(sa.app.Config().Network, portSpec) + ln, err = utils.NewMultiAddrListener(sa.app.Config().Network, portSpec, utils.ListenerOptions{SocketPerm: sa.socketPerm}) } if err != nil { return fmt.Errorf("failed to bind s3 listener %s: %w", portSpec, err) diff --git a/s3api/utils/multi_listener.go b/s3api/utils/multi_listener.go index 8f718709..d03f9402 100644 --- a/s3api/utils/multi_listener.go +++ b/s3api/utils/multi_listener.go @@ -272,6 +272,15 @@ func resolveHostnameAddrs(address string) ([]string, error) { return addrs, nil } +// ListenerOptions configures optional behaviour for NewMultiAddrListener and +// NewMultiAddrTLSListener. +type ListenerOptions struct { + // SocketPerm, when non-zero, sets the file-mode permissions on file-backed + // UNIX sockets after binding. It is ignored for TCP/IP addresses and + // abstract namespace sockets. + SocketPerm os.FileMode +} + // NewMultiAddrListener creates listeners for all IP addresses that the hostname // in the address resolves to. If the address is already an IP, it creates a // single listener. Returns a MultiListener if multiple addresses are resolved, @@ -281,7 +290,10 @@ func resolveHostnameAddrs(address string) ([]string, error) { // - "/path/to/socket" or "./rel/socket" — file-backed socket; any stale // socket file is removed before binding. // - "@name" — Linux abstract namespace socket; no file is created or removed. -func NewMultiAddrListener(network, address string) (net.Listener, error) { +// +// opts.SocketPerm, when non-zero, sets the file-mode permissions on file-backed +// sockets after binding. It is ignored for TCP/IP addresses and abstract sockets. +func NewMultiAddrListener(network, address string, opts ListenerOptions) (net.Listener, error) { if IsUnixSocketPath(address) { // For file-backed sockets, remove any stale socket file so re-binding works cleanly. // Abstract sockets (@name) have no filesystem entry; skip removal for them. @@ -294,6 +306,12 @@ func NewMultiAddrListener(network, address string) (net.Listener, error) { if err != nil { return nil, fmt.Errorf("failed to bind unix socket listener %s: %w", address, err) } + if opts.SocketPerm != 0 && !isAbstractSocket(address) { + if err := os.Chmod(address, opts.SocketPerm); err != nil { + ln.Close() + return nil, fmt.Errorf("failed to set permissions on socket %s: %w", address, err) + } + } return NewMultiListener(ln), nil } @@ -328,7 +346,10 @@ func NewMultiAddrListener(network, address string) (net.Listener, error) { // - "/path/to/socket" or "./rel/socket" — file-backed socket; any stale // socket file is removed before binding. // - "@name" — Linux abstract namespace socket; no file is created or removed. -func NewMultiAddrTLSListener(network, address string, getCertificateFunc func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (net.Listener, error) { +// +// opts.SocketPerm, when non-zero, sets the file-mode permissions on file-backed +// sockets after binding. It is ignored for TCP/IP addresses and abstract sockets. +func NewMultiAddrTLSListener(network, address string, getCertificateFunc func(*tls.ClientHelloInfo) (*tls.Certificate, error), opts ListenerOptions) (net.Listener, error) { config := &tls.Config{ MinVersion: tls.VersionTLS12, GetCertificate: getCertificateFunc, @@ -344,6 +365,12 @@ func NewMultiAddrTLSListener(network, address string, getCertificateFunc func(*t if err != nil { return nil, fmt.Errorf("failed to bind unix TLS socket listener %s: %w", address, err) } + if opts.SocketPerm != 0 && !isAbstractSocket(address) { + if err := os.Chmod(address, opts.SocketPerm); err != nil { + ln.Close() + return nil, fmt.Errorf("failed to set permissions on socket %s: %w", address, err) + } + } return NewMultiListener(tls.NewListener(ln, config)), nil } diff --git a/s3api/utils/multi_listener_test.go b/s3api/utils/multi_listener_test.go index 4729bf94..6e7b000b 100644 --- a/s3api/utils/multi_listener_test.go +++ b/s3api/utils/multi_listener_test.go @@ -301,7 +301,7 @@ func TestNewMultiAddrListener(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ln, err := NewMultiAddrListener("tcp", tt.address) + ln, err := NewMultiAddrListener("tcp", tt.address, ListenerOptions{}) if (err != nil) != tt.wantErr { t.Errorf("NewMultiAddrListener() error = %v, wantErr %v", err, tt.wantErr) return @@ -353,7 +353,7 @@ func TestNewMultiAddrTLSListener(t *testing.T) { return &cert, err } - ln, err := NewMultiAddrTLSListener("tcp", "127.0.0.1:0", getCertFunc) + ln, err := NewMultiAddrTLSListener("tcp", "127.0.0.1:0", getCertFunc, ListenerOptions{}) if err != nil { t.Fatalf("NewMultiAddrTLSListener() error = %v", err) } diff --git a/webui/webserver.go b/webui/webserver.go index c4146157..cbafd897 100644 --- a/webui/webserver.go +++ b/webui/webserver.go @@ -19,6 +19,7 @@ import ( "fmt" "net" "net/http" + "os" "strings" "github.com/gofiber/fiber/v2" @@ -43,6 +44,7 @@ type Server struct { config *ServerConfig pathPrefix string quiet bool + socketPerm os.FileMode } // Option sets various options for NewServer() @@ -63,6 +65,13 @@ func WithPathPrefix(prefix string) Option { return func(s *Server) { s.pathPrefix = prefix } } +// WithSocketPerm sets the file-mode permissions applied to file-backed UNIX +// domain sockets after binding. It has no effect on TCP/IP or abstract +// namespace sockets. +func WithSocketPerm(perm os.FileMode) Option { + return func(s *Server) { s.socketPerm = perm } +} + // NewServer creates a new GUI server instance func NewServer(cfg *ServerConfig, opts ...Option) *Server { app := fiber.New(fiber.Config{ @@ -175,9 +184,9 @@ func (s *Server) ServeMultiPort(ports []string) error { var err error if s.CertStorage != nil { - ln, err = utils.NewMultiAddrTLSListener(s.app.Config().Network, addrSpec, s.CertStorage.GetCertificate) + ln, err = utils.NewMultiAddrTLSListener(s.app.Config().Network, addrSpec, s.CertStorage.GetCertificate, utils.ListenerOptions{SocketPerm: s.socketPerm}) } else { - ln, err = utils.NewMultiAddrListener(s.app.Config().Network, addrSpec) + ln, err = utils.NewMultiAddrListener(s.app.Config().Network, addrSpec, utils.ListenerOptions{SocketPerm: s.socketPerm}) } if err != nil {