There is nothing wrong with your approach but suid binaries have a poor reputation...
Afaik there are at least three ways to do this.
- start as root and drop privileges for parts that don't need root
- allow some user to run a command as root with sudo
- 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, "")
}