Skip to content

Commit

Permalink
feat: Drop root privileges
Browse files Browse the repository at this point in the history
  • Loading branch information
dadav committed Jul 8, 2024
1 parent 6ad6226 commit 76039d8
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 20 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ Flags:
--cache-prefixes string url prefixes to cache (default "/v3/files")
--cors string allowed cors origins separated by comma (default "*")
--dev enables dev mode
--drop-privileges drops privileges to the given user/group
--fallback-proxy string optional comma separated list of fallback upstream proxy urls
--group string give control to this group or gid (requires root)
-h, --help help for serve
--import-proxied-releases add every proxied modules to local store
--jwt-secret string jwt secret (default "changeme")
Expand All @@ -103,6 +105,7 @@ Flags:
--port int the port to listen to (default 8080)
--tls-cert string path to tls cert file
--tls-key string path to tls key file
--user string give control to this user or uid (requires root)
Global Flags:
--config string config file (default is $HOME/.gorge.yaml)
Expand Down Expand Up @@ -148,6 +151,10 @@ Via file (`$HOME/.config/gorge.yaml` or `./gorge.yaml`):

```yaml
---
# Set uid of process to this users uid
user: ""
# Set gid of process to this groups gid
group: ""
# The forge api version to use. Currently only v3 is supported.
api-version: v3
# The backend type to use. Currently only filesystem is supported.
Expand All @@ -162,6 +169,8 @@ cache-prefixes: /v3/files
cors: "*"
# Enables the dev mode.
dev: false
# Drop privileges if running as root (user & group options must be set)
drop-privileges: false
# List of comma separated upstream forge(s) to use when local requests return 404
fallback-proxy:
# Import proxied modules into local backend.
Expand All @@ -187,13 +196,16 @@ tls-key: ""
Via environment:

```bash
GORGE_USER=""
GORGE_GROUP=""
GORGE_API_VERSION=v3
GORGE_BACKEND=filesystem
GORGE_BIND=127.0.0.1
GORGE_CACHE_MAX_AGE=86400
GORGE_CACHE_PREFIXES=/v3/files
GORGE_CORS="*"
GORGE_DEV=false
GORGE_DROP_PRIVILEGES=false
GORGE_FALLBACK_PROXY=""
GORGE_IMPORT_PROXIED_RELEASES=false
GORGE_MODULESDIR=~/.gorge/modules
Expand All @@ -218,6 +230,13 @@ in the Authorization header like this:

In dev mode these security checks are disabled.

### 💧 Dropping privileges

There is no need to run gorge as root. But if you still want to do it, be sure to
use the `--drop-privileges` option combined with `--user` and `--group`. You could
set these to `www-data`. It will ensure gorge won't keep running as root, after the
required root actions are done.
## 🐝 Development
The code template for `v3` was generated with this command:
Expand Down
99 changes: 79 additions & 20 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@ package cmd
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"os/user"
"slices"
"strconv"
"strings"
"syscall"
"time"

config "github.com/dadav/gorge/internal/config"
log "github.com/dadav/gorge/internal/log"
customMiddleware "github.com/dadav/gorge/internal/middleware"
"github.com/dadav/gorge/internal/utils"
v3 "github.com/dadav/gorge/internal/v3/api"
backend "github.com/dadav/gorge/internal/v3/backend"
openapi "github.com/dadav/gorge/pkg/gen/v3/openapi"
Expand All @@ -40,7 +44,6 @@ import (
"github.com/go-chi/cors"
"github.com/go-chi/jwtauth/v5"
"github.com/go-chi/stampede"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
Expand All @@ -60,19 +63,19 @@ You can also enable the caching functionality to speed things up.`,

log.Setup(config.Dev)

config.ModulesDir, err = homedir.Expand(config.ModulesDir)
config.ModulesDir, err = utils.ExpandTilde(config.ModulesDir)
if err != nil {
log.Log.Fatal(err)
}
config.TlsCertPath, err = homedir.Expand(config.TlsCertPath)
config.TlsCertPath, err = utils.ExpandTilde(config.TlsCertPath)
if err != nil {
log.Log.Fatal(err)
}
config.TlsKeyPath, err = homedir.Expand(config.TlsKeyPath)
config.TlsKeyPath, err = utils.ExpandTilde(config.TlsKeyPath)
if err != nil {
log.Log.Fatal(err)
}
config.JwtTokenPath, err = homedir.Expand(config.JwtTokenPath)
config.JwtTokenPath, err = utils.ExpandTilde(config.JwtTokenPath)
if err != nil {
log.Log.Fatal(err)
}
Expand All @@ -88,6 +91,31 @@ You can also enable the caching functionality to speed things up.`,
if err != nil {
log.Log.Fatal(err)
}
if config.DropPrivileges && utils.IsRoot() {
uid, err := strconv.Atoi(config.User)
if err != nil {
u, err := user.Lookup(config.User)
if err != nil {
log.Log.Fatal(err)
}
uid, err = strconv.Atoi(u.Uid)
if err != nil {
log.Log.Fatal(err)
}
}
gid, err := strconv.Atoi(config.Group)
if err != nil {
g, err := user.LookupGroup(config.Group)
if err != nil {
log.Log.Fatal(err)
}
gid, err = strconv.Atoi(g.Gid)
if err != nil {
log.Log.Fatal(err)
}
}
os.Chown(config.ModulesDir, uid, gid)
}
}

if config.ApiVersion == "v3" {
Expand Down Expand Up @@ -119,14 +147,12 @@ You can also enable the caching functionality to speed things up.`,
return r.Method != "GET"
}))

if _, err = os.Stat(config.JwtTokenPath); err != nil {
_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user": "admin"})
err = os.WriteFile(config.JwtTokenPath, []byte(tokenString), 0400)
if err != nil {
log.Log.Fatal(err)
}
log.Log.Infof("JWT token was written to %s\n", config.JwtTokenPath)
_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user": "admin"})
err = os.WriteFile(config.JwtTokenPath, []byte(tokenString), 0400)
if err != nil {
log.Log.Fatal(err)
}
log.Log.Infof("JWT token was written to %s", config.JwtTokenPath)
}

if !config.NoCache {
Expand All @@ -139,7 +165,6 @@ You can also enable the caching functionality to speed things up.`,
}

if config.FallbackProxyUrl != "" {

proxies := strings.Split(config.FallbackProxyUrl, ",")
slices.Reverse(proxies)

Expand Down Expand Up @@ -191,9 +216,6 @@ You can also enable the caching functionality to speed things up.`,
w.Write([]byte(`{"message": "ok"}`))
})

bindPort := fmt.Sprintf("%s:%d", config.Bind, config.Port)
log.Log.Infof("Listen on %s", bindPort)

ctx, restoreDefaultSignalHandling := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer restoreDefaultSignalHandling()
g, gCtx := errgroup.WithContext(ctx)
Expand All @@ -220,15 +242,49 @@ You can also enable the caching functionality to speed things up.`,
}
}

server := http.Server{Addr: bindPort, Handler: r, BaseContext: func(_ net.Listener) context.Context { return ctx }}
bindPort := fmt.Sprintf("%s:%d", config.Bind, config.Port)
listener, err := net.Listen("tcp", bindPort)
if err != nil {
log.Log.Fatal(err)
}
log.Log.Infof("Listen on %s", bindPort)

server := http.Server{Handler: r, BaseContext: func(_ net.Listener) context.Context { return ctx }}
wantTLS := config.TlsKeyPath != "" && config.TlsCertPath != ""

if wantTLS {
certificate, err := os.ReadFile(config.TlsCertPath)
if err != nil {
log.Log.Fatal(err)
}
key, err := os.ReadFile(config.TlsKeyPath)
if err != nil {
log.Log.Fatal(err)
}
cert, err := tls.X509KeyPair(certificate, key)
if err != nil {
log.Log.Fatal(err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}
server.TLSConfig = tlsConfig
}

if config.DropPrivileges && utils.IsRoot() {
log.Log.Infof("Give control to user %s and group %s", config.User, config.Group)
if err = utils.DropPrivileges(config.User, config.Group); err != nil {
log.Log.Fatal(err)
}
}

g.Go(func() error {
if config.TlsKeyPath != "" && config.TlsCertPath != "" {
if err := server.ListenAndServeTLS(config.TlsCertPath, config.TlsKeyPath); err != http.ErrServerClosed {
if wantTLS {
if err := server.ServeTLS(listener, "", ""); err != http.ErrServerClosed {
return err
}
} else {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
if err := server.Serve(listener); err != http.ErrServerClosed {
return err
}
}
Expand Down Expand Up @@ -257,6 +313,8 @@ You can also enable the caching functionality to speed things up.`,
func init() {
rootCmd.AddCommand(serveCmd)

serveCmd.Flags().StringVar(&config.User, "user", "", "give control to this user or uid (requires root)")
serveCmd.Flags().StringVar(&config.Group, "group", "", "give control to this group or gid (requires root)")
serveCmd.Flags().StringVar(&config.ApiVersion, "api-version", "v3", "the forge api version to use")
serveCmd.Flags().IntVar(&config.Port, "port", 8080, "the port to listen to")
serveCmd.Flags().StringVar(&config.Bind, "bind", "127.0.0.1", "host to listen to")
Expand All @@ -266,6 +324,7 @@ func init() {
serveCmd.Flags().StringVar(&config.CORSOrigins, "cors", "*", "allowed cors origins separated by comma")
serveCmd.Flags().StringVar(&config.FallbackProxyUrl, "fallback-proxy", "", "optional comma separated list of fallback upstream proxy urls")
serveCmd.Flags().BoolVar(&config.Dev, "dev", false, "enables dev mode")
serveCmd.Flags().BoolVar(&config.DropPrivileges, "drop-privileges", false, "drops privileges to the given user/group")
serveCmd.Flags().StringVar(&config.CachePrefixes, "cache-prefixes", "/v3/files", "url prefixes to cache")
serveCmd.Flags().StringVar(&config.JwtSecret, "jwt-secret", "changeme", "jwt secret")
serveCmd.Flags().StringVar(&config.JwtTokenPath, "jwt-token-path", "~/.gorge/token", "jwt token path")
Expand Down
6 changes: 6 additions & 0 deletions defaults.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
---
# Set uid of process to this users uid
user: ""
# Set gid of process to this groups gid
group: ""
# The forge api version to use. Currently only v3 is supported.
api-version: v3
# The backend type to use. Currently only filesystem is supported.
Expand All @@ -13,6 +17,8 @@ cache-prefixes: /v3/files
cors: "*"
# Enables the dev mode.
dev: false
# Drop privileges if running as root (user & group options must be set)
drop-privileges: false
# List of comma separated upstream forge(s) to use when local requests return 404
fallback-proxy:
# Import proxied modules into local backend.
Expand Down
3 changes: 3 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package config

var (
User string
Group string
ApiVersion string
Port int
Bind string
Dev bool
DropPrivileges bool
ModulesDir string
ModulesScanSec int
Backend string
Expand Down
59 changes: 59 additions & 0 deletions internal/utils/privileges.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build !windows
// +build !windows

package utils

import (
"errors"
"os"
"os/user"
"strconv"
"syscall"
)

func IsRoot() bool {
return os.Geteuid() == 0
}

func DropPrivileges(newUid, newGid string) error {
if newUid == "" {
return errors.New("user option is empty, cant drop privileges")
}

if newGid == "" {
return errors.New("group option is unset, cant drop privileges")
}

gid, err := strconv.Atoi(newGid)
if err != nil {
g, err := user.LookupGroup(newGid)
if err != nil {
return err
}
gid, err = strconv.Atoi(g.Gid)
if err != nil {
return err
}
}

if err = syscall.Setgid(gid); err != nil {
return err
}

uid, err := strconv.Atoi(newUid)
if err != nil {
u, err := user.Lookup(newUid)
if err != nil {
return err
}
uid, err = strconv.Atoi(u.Uid)
if err != nil {
return err
}
}
if err = syscall.Setuid(uid); err != nil {
return err
}

return nil
}
16 changes: 16 additions & 0 deletions internal/utils/privileges_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build windows
// +build windows

package utils

import (
"errors"
)

func IsRoot() bool {
return false
}

func DropPrivileges(newUid, newGid string) error {
return errors.New("cant drop privileges in windows")
}
15 changes: 15 additions & 0 deletions internal/utils/tilde.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package utils

import (
"os/user"
"strings"
)

// ExpandTilde replaces ~ with the homedir of the current user
func ExpandTilde(path string) (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
return strings.Replace(path, "~", u.HomeDir, 1), nil
}

0 comments on commit 76039d8

Please sign in to comment.