Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this a reasonable way to run sections of go code as root on Linux?

Tags:

go

What is the best way to run sections of my go code as root on Linux? I am writing a sizable web app and do not want to run the whole app as root. Some of the code needs to manipulate the network stack, firewall rules, etc and therefore must run as root.

Currently, I have created a small stub executable that calls code back in my larger app. I then compile the stub and set the Linux suid bit. I then call it from my main go app as a shell command.

It works. But is this the most efficient way? Is there an idiomatic way?

Plan B was to use a REST API from a small HTTP service. Though that is a little harder to secure.

Thoughts?

e.g... the exec doupdate has the suid set for root permissions

From within the web app...

// Elevate UpdateNft to root
func ElevateUpdateNFT() string {
    out, err := exec.Command("doupdate", "nft").CombinedOutput()
    if err != nil {
        log.Print("exec doupdate ", err)
    }
    if len(out) > 0 {
        log.Print(string(out))
    }
    return string(out)
}

Inside doupdate, executing as root... CallUpdateNft is back in the main web app but now running with root permissions.

func main() {
...
    case key == "nft":
        models.CallUpdateNft()
...
}
like image 374
PrecisionPete Avatar asked Dec 22 '25 15:12

PrecisionPete


1 Answers

There is nothing wrong with your approach but suid binaries have a poor reputation...


Afaik there are at least three ways to do this.

  1. start as root and drop privileges for parts that don't need root
  2. allow some user to run a command as root with sudo
  3. make a privileged process do something

One

This will run the web server without privileges.


cmd := exec.Command("/bin/webserver", "--foo", "--bar")
cmd.SysProcAttr = &syscall.SysProcAttr{
            Credential: &syscall.Credential{
                Gid: uint32(33),
                Uid: uint32(33)}}
cmd.Start();

Two

/etc/sudoers.d/www-data :

www-data ALL=(ALL) NOPASSWD: /bin/webserver

cmd := exec.Command("sudo", "/bin/webserver", "--foo", "--bar")
cmd.Start();

Three

Communicate to a privileged process and make it do stuff.

$ go run both.go library.go  /etc/shadow
0
open /etc/shadow: permission denied

server as root:

$ make serve 
go build -o server server.go library.go ; sudo ./server

then connect:

$ ./client /etc/shadow  | wc -l
1918
ok.
69

Makefile

all: stop
    go build -o client client.go library.go
    go build -o server server.go library.go
    ./server & sleep 0.2
    ./client $(path)
both:
    go build -o sc both.go library.go && ./sc $(path)
stop: ; killall server || true
serve: ; go build -o server server.go library.go ; sudo ./server
clean: ; rm -vf client sc server

both.go

package main

import (
    "fmt"
    "os"
    "sync"
)

func main() {
    wg := &sync.WaitGroup{}
    wg.Add(1)
    go Server(wg, "")


    path := "main.go"
    if len(os.Args) > 1 {
        path = os.Args[1]
    }
    resp, err := Client(path)

    if nil == err {
        fmt.Print(resp.Output)
    } else {
        println(err.Error())
    }
    wg.Wait()

}

client.go

package main

import (
    "fmt"
    "os"
)

func main() {
    path := "/dev/null"
    if len(os.Args) > 1 {
        path = os.Args[1]
    }
    resp, err := Client(path)
    if nil == err {
        println("ok.")
        fmt.Print(resp.Output)
    } else {
        println(err.Error())
    }

}

library.go

package main

import (
    "net"
    "net/rpc"
    "os"
    "sync"
    "time"
)

const Addr = "localhost:31337"

type Api struct {
    server *rpc.Server
    quit   *sync.WaitGroup
}

func newApi(address string, wg *sync.WaitGroup) (*Api, error) {
    if len(address) == 0 {
        address = Addr
    }
    rpcs := rpc.NewServer()
    errR := rpcs.RegisterName("V1", &MethodsV1{ Quit : wg })
    sock, errL := net.Listen("tcp", address)
    if errR != nil {
        return nil, errR
    }
    if errL != nil {
        return nil, errL
    }
    go rpcs.Accept(sock)
    return &Api{server: rpcs, quit: wg}, nil
}

func Server(wg *sync.WaitGroup, addr string) {
    _, err := newApi(addr, wg)
    if err != nil {
        return
    }
    if wg != nil {
        wg.Wait()
    } else {
        <- time.After(time.Second * 50)
    }
}

func Client(path string) (RpcResponse, error) {
    addr := Addr
    if a := os.Getenv("ADDR"); len(a) > 0 {
        addr = a
    }
    resp := RpcResponse{}
    client, errC := rpc.Dial("tcp", addr)
    if nil == errC {
        errC = client.Call("V1.Cat", &path, &resp);
        println(len(resp.Output))
    }
    return resp, errC
}

type RpcResponse struct { Output string }
type MethodsV1 struct {
    Quit *sync.WaitGroup
}

func (m1 *MethodsV1) Cat(path *string, response *RpcResponse) error {
    if m1.Quit != nil {
        defer m1.Quit.Done()
    }
    o, err := os.ReadFile(*path)
    response.Output = string(o)
    return err
}

server.go

package main

func main() {
    Server(nil, "")
}
like image 118
Ярослав Рахматуллин Avatar answered Dec 24 '25 08:12

Ярослав Рахматуллин